比赛链接https://codeforces.com/contest/1988
A. Split the Multiset
题意
起初$S$中只有一个数字 $n$ , 你每次操作可以选择$S$中的一个任意一个数$x$,将他拆分成最多k个数,并且这k个数的和仍为$x$ ,问最少需要多少次拆分,能让$S$中只剩下$n$个$1$
思路
可以发现,每次拆分出$k-1$ 个 1 效率最高。 所以对于$n,k$ ,我们有总拆分次数为$\lceil \frac{n-1}{k-1} \rceil$ 。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include<bits/stdc++.h> using namespace std; #define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0); #define int long long #define rep(i,l,r) for(int i = l;i<=r;i++) #define per(i,r,l) for(int i = r;i>=l;i--) const int INF = 0x3f3f3f3f3f3f3f3f; typedef pair<int,int> PII; void solve(){ int n,k; cin>>n>>k; cout<< (n -1 + k - 2) / (k - 1)<<endl; } signed main(){ int T = 1; cin>>T; while(T--){ solve(); } return 0; }
|
B. Make Majority
题意
给你一个长度为n的01序列$a$,你可以进行任意次如下操作:
- 选择两个数$l,r (1\le l\le r \le |a|)$ , 其中 $|a|$ 表示a的长度。
- 将这段区间替换为$c$, (如果选择的区间中1的数量大于0的数量,那么c=1,否则c=0)
问能否在进行一定次数的操作后, 使得a变为 1
思路
首先我们可以先选择连续的0 , 这样这些连续的0就一定会变为一个0 。这样每个0周围都会有1~2个1 。 我们说此刻的序列为$a’$
于是我们每次可以选择一个0,以及它旁边的两个1, 这样这三个数就会被替换为。不断循环往复, 每次替换都会减少一个1以及一个0。 因此我们直接统计$a’$ 中1和0的数量,若1的数量更多那么就输出$Yes$, 否则输出$No$
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include<bits/stdc++.h> using namespace std; #define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0); #define int long long #define rep(i,l,r) for(int i = l;i<=r;i++) #define per(i,r,l) for(int i = r;i>=l;i--) const int INF = 0x3f3f3f3f3f3f3f3f; typedef pair<int,int> PII; void solve(){ int n; cin>>n; string str; cin>>str; string s; rep(i,0,n-1){ if(str[i] == '0'){ s += str[i]; while(i<n && str[i] == '0') i++; } if(i == n) continue; s += str[i]; } int c0 = 0,c1 = 0; for(auto p : s){ if( p == '0') c0 ++; else c1 ++; } if(c0 >= c1) cout<<"No\n"; else cout<<"Yes\n"; } signed main(){ int T = 1; cin>>T; while(T--){ solve(); } return 0; }
|
C. Increasing Sequence with Fixed OR
题意
给你一个数$n$ , 请你构造一个数列$a$ , 使得$1 \le a1 < a_2 < … < a_k$ , 并且对于任意的$ 2\le i \le k$ 都有 $a{i-1} | a_i = n$ 其中|
表示按位或运算。要求你构造的$k$尽可能大。
思路
因为要求相邻的数按位或都是n, 所以对于任意的 $a_i$ ,一定有$a_i | n = n$, 即$a_i$ 一定是从二进制的n中选择一些1来组成。于是我们便只需要考虑n为全1的情况即可(因为n中的数位0在 $a_i$ 中一定为$0$ )
- 因为$ai | a{i+1} = n$ 所以对于某一位$j$ ,若$a{i,j} = 0$ 那么 一定有$a{i+1,j} = 1$ 。
- 再根据另一个条件$1 \le a1 < a_2 < … < a_k$ ,我们可以得知,当$a{i,j} = 1$ 时 ,那么$a{i+1,j},a{i+2,j},…,a_{k,j}$ 都是1。
于是我们便可以这样构造我们的数列:01111,10111,11011,11101,11110,11111
, 于是若n的二进制中中有t个1,那么我们构造的数列长度便为$t+1$ . 构造方法即 让0从高到低地位于t个位中的每一位,最后再加上n本身。
需要注意的是,如果n是二进制数, 即n中只有一位1,那么答案是1,数列的值为n本身。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include<bits/stdc++.h> using namespace std; #define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0); #define int long long #define rep(i,l,r) for(int i = l;i<=r;i++) #define per(i,r,l) for(int i = r;i>=l;i--) const int INF = 0x3f3f3f3f3f3f3f3f; typedef pair<int,int> PII;
void solve(){ int n; cin>>n; vector<int> v; for(int i = 0;i<63;i++){ if((1ll<<i) & n){ v.push_back(i); } } if(v.size() == 1) {cout<<1<<'\n'<<n<<'\n';return ;} cout<<(v.size()+1)<<endl; for(int i = v.size()-1;i>=0;i--){ int t = n; t ^= (1ll<<v[i]); cout<<t<<" "; } cout<<n<<'\n';
} signed main(){ int T = 1; cin>>T; while(T--){ solve(); } return 0; }
|
D. The Omnipotent Monster Killer
题意
有$n$头怪兽, 每个怪兽都有其自己的攻击力,他们通过$n-1$条边连成一个树。 你可以进行足够多的回合。在每个回合中, 会进行如下过程:
- 所有活着的怪兽都对你造成一次等同于其攻击力的伤害
- 你选择一些怪兽(这些怪兽不能相邻),将他们杀死
问最少受到多少伤害,你才能够杀死所有的怪物。
思路
首先可以发现,对于n头怪兽,我们最多需要 $log_2n$ 个回合就可以在最优解的情况下将他们全部击杀。(考虑完全树)。
这样对于$n < 3 * 10^5 $ ,总回合数最多19个回合就可以将他们全部击败。
于是我们考虑树形dp, 设定节点1为$dp[i][j]$ 表示节点$i$在第$j$ 个回合被杀死,那么以它为根节点的子树所造成的总伤害。
于是有:
最终答案:$ans = min_{1\le j \le 19} dp[1][j]$
状态转移:$dp[i][j] = \sum{v\in sons(i)} min{1\le k\le 19 \& k \not=j} dp[v][k] + ack[i] * j$
复杂度$O(n * log^2n)$
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| #include<bits/stdc++.h> using namespace std; #define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0); #define int long long #define rep(i,l,r) for(int i = l;i<=r;i++) #define per(i,r,l) for(int i = r;i>=l;i--) const int INF = 0x3f3f3f3f3f3f3f3f; typedef pair<int,int> PII; int dp[300005][25]; int ack[300005]; vector<int> edge[300005]; void dfs(int u,int fa){ for(int i = 1;i<=22;i++){ dp[u][i] = INF; } for(auto p : edge[u]){ if(p == fa) continue; dfs(p,u); } for(int j = 1;j<=22;j++){ bool f = 0; dp[u][j] = 0; for(auto p : edge[u]){ if(p == fa) continue; f = 1; int mn = INF; for(int k = 1;k<=22;k++){ if(k == j) continue; mn = min(mn,dp[p][k]); } dp[u][j] += mn; } dp[u][j] += ack[u] * j; } return ; } void solve(){ int n; cin>>n; rep(i,1,n) cin>>ack[i]; if(n == 1) {cout<<ack[1] << endl; return ;} rep(i,1,n) edge[i].clear(); for(int i = 1;i<n;i++){ int x ,y;cin>>x>>y; edge[x].push_back(y); edge[y].push_back(x); } dfs(1,0); int ans = INF; for(int i= 1;i<=22;i++){ ans = min(ans,dp[1][i]); } cout<<ans<<'\n'; } signed main(){ int T = 1; cin>>T; while(T--){ solve(); } return 0; }
|
优化$O(nlogn)$
对于递推过程,如果我们用前缀/后缀 数组记录 前缀最小值,以及后缀最小值。
即$pre[u][i] = MIN{1\le j\le i} dp[u][j] \lst[u][i] = MIN{i\le j\le 19} dp[u][j]$
于是我们在递推时就可以直接写$dp[i][j] = \sum_{v\in sons(i)} min(pre[v][j-1],lst[v][j+1]) + ack[i] * j$ 。
我们就可以省去一层循环。 实现复杂度$O(nlogn)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| #include<bits/stdc++.h> using namespace std; #define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0); #define int long long #define rep(i,l,r) for(int i = l;i<=r;i++) #define per(i,r,l) for(int i = r;i>=l;i--) const int INF = 0x3f3f3f3f3f3f3f3f; typedef pair<int,int> PII; int dp[300005][25]; int ack[300005]; vector<int> edge[300005]; int pre[300005][25]; int lst[300005][25]; void dfs(int u,int fa){ for(int i = 1;i<= 19;i++){ dp[u][i] = INF; } for(auto p : edge[u]){ if(p == fa) continue; dfs(p,u); } for(int j = 1;j<= 19;j++){ bool f = 0; dp[u][j] = 0; for(auto p : edge[u]){ if(p == fa) continue; f = 1; int mn = min(pre[p][j-1],lst[p][j+1]); dp[u][j] += mn; } dp[u][j] += ack[u] * j; } pre[u][0] = INF; for(int i = 1;i<= 19;i++){ pre[u][i] = min(dp[u][i],pre[u][i-1]); } lst[u][20] = INF; for(int i = 19;i>=1;i--){ lst[u][i] = min(dp[u][i],lst[u][i+1]); } return ; } void solve(){ int n; cin>>n; rep(i,1,n) cin>>ack[i]; if(n == 1) {cout<<ack[1] << endl; return ;} rep(i,1,n) edge[i].clear(); for(int i = 1;i<n;i++){ int x ,y;cin>>x>>y; edge[x].push_back(y); edge[y].push_back(x); } dfs(1,0); int ans = INF; for(int i= 1;i<= 19;i++){ ans = min(ans,dp[1][i]); } cout<<ans<<'\n'; } signed main(){ int T = 1; cin>>T; while(T--){ solve(); } return 0; }
|
优化$O(n)$
如果再细心一些,可以发现,对于一个节点$i$ , 设它的度数为$deg_i$ , 它会在第$b_i$ 轮被杀死, 那么一定有$b_i \le deg_i$ 。于是我们没必要对每个结点都循环到19, 只需要循环到它对应的度数即可。