Codeforces Round #595 (Div. 3)
A - Yet Another Dividing into Teams
题意:n个不同数,分尽可能少的组,要求组内没有两个人的差恰为1。
题解:奇偶分组。
int a[200005];
void test_case() {
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
sort(a + 1, a + 1 + n);
int ans = 1;
for(int i = 2; i <= n; ++i) {
if(a[i] == a[i - 1] + 1)
ans = 2;
}
printf("%d\n", ans);
}
B1 - Books Exchange (hard version)
见下
B2 - Books Exchange (hard version)
题意:有一个排列,求每个位置的环的大小。
题解:并查集或者直接dfs。
int p[200005];
int ans[200005];
int cnt;
void dfs(int u, int t) {
if(u == t)
return;
++cnt;
dfs(p[u], t);
ans[u] = cnt;
}
void test_case() {
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i) {
scanf("%d", &p[i]);
ans[i] = 0;
}
for(int i = 1; i <= n; ++i) {
if(!ans[i]) {
cnt = 1;
dfs(p[i], i);
ans[i] = cnt;
}
printf("%d%c", ans[i], " \n"[i == n]);
}
}
C1 - Good Numbers (easy version)
见下
C2 - Good Numbers (hard version)
题意:一个good number是有一系列互异的3的幂次的和构成的,求超过n的最小的good number。
题解:这么说每种幂次都至多出现1次,先让大家都出现,然后优先去掉最大的幂次。
ll p3[105];
void test_case() {
p3[0] = 1;
int top = 0;
while(p3[top] < 1e18)
p3[++top] = p3[top - 1] * 3ll;
ll sum = 0;
for(int i = 0; i <= top; ++i) {
//cout<<p3[i]<<endl;
sum += p3[i];
}
int q;
scanf("%d", &q);
while(q--) {
ll n;
scanf("%lld", &n);
ll tsum = sum;
for(int i = top; i >= 0; --i) {
if(tsum - p3[i] >= n)
tsum -= p3[i];
}
printf("%lld\n", tsum);
}
}
D1 - Too Many Segments (easy version)
见下
D2 - Too Many Segments (hard version)
题意:给一个数轴,上面有n个线段,移除最少的线段使得没有任何一个点被覆盖超过k次。(包括端点)
题解:看起来可以口胡一个贪心,先用线段树update出每个点的被覆盖次数,然后在每个点记录覆盖当前点的最右的线段是哪个(不过这样好像会记录太多线段),从左往右扫描,当一个点超过k时就移除最右的那条线段。这个做法的问题在于不能维护最右的线段是哪个。不过我们从左往右扫的时候遇到左端点就把线段的右端点插进set里面,然后遇到线段的右端点就可以把它移除(其实不移除也可以)。仔细想想甚至不需要线段树,也不需要set。
int n, k;
int l[200005], r[200005];
vector<int> L[200005], R[200005];
const int N = 200000;
bool del[200005];
priority_queue<pii> S;
vector<int> ans;
void test_case() {
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; ++i) {
scanf("%d%d", &l[i], &r[i]);
L[l[i]].push_back(i);
R[r[i]].push_back(i);
}
int cnt = 0;
for(int i = 1; i <= N; ++i) {
for(auto &t : L[i]) {
++cnt;
S.push({r[t], t});
}
while(cnt > k) {
--cnt;
int t = S.top().second;
S.pop();
del[t] = 1;
ans.push_back(t);
}
for(auto &t : R[i]) {
if(!del[t])
--cnt;
}
}
printf("%d\n", (int)ans.size());
for(auto &t : ans)
printf("%d ", t);
printf("\n");
}
E - By Elevator or Stairs?
题意:有一个n层建筑,求从第1层去第i层的最短时间。你可以选择走楼梯,或者支付一个等待时间之后走电梯。
题解:那只需要记录前一层最后在楼梯和在电梯的最小花费直接转移就行了。
大意了,没看范围就交了,还好不会溢出。
int a[200005];
int b[200005];
void test_case() {
int n, c;
scanf("%d%d", &n, &c);
for(int i = 1; i <= n - 1; ++i)
scanf("%d", &a[i]);
for(int i = 1; i <= n - 1; ++i)
scanf("%d", &b[i]);
int dp0 = 0, dp1 = c;
for(int i = 1; i <= n; ++i) {
if(i > 1) {
int ndp0 = min(dp0, dp1) + a[i - 1];
int ndp1 = min(dp0 + c + b[i - 1], dp1 + b[i - 1]);
dp0 = ndp0;
dp1 = ndp1;
}
printf("%d%c", min(dp0, dp1), " \n"[i == n]);
}
}
F - Maximum Weight Subset
题意:在一棵树上面找一个点集,使得点集的权重和最大且点集中的点两两之间的距离严格大于k。
题解:感觉是可以树形dp的,设dp[i][j]为以结点i为根的子树,最近的被选择点距离为j的最大的权值。当i被选择时j就是0;否则当i的儿子被选择时j就是1。
但是子树之间的选择是比较复杂的。先把第一棵子树之间合并到根上:dp[u][j+1]=dp[i][j],且dp[u][0]=dp[i][k]。
再遍历u的其他子树i,距离为j的子树可以向根的距离t>=k-j配对,这时尝试更新dp[u][min(j+1,t)]。
看起来是个n^3的dp。
但是这个实际上有一些细节,比方说我这棵子树只有很底部的选了,而根节点不选,这个时候距离是>=k+1的状态,这些应该统一存起来。
int n, k;
int w[205];
int dp[205][205];
vector<int>G[205];
void dfs(int u, int p) {
if(G[u].size() == 1 && p != -1) {
dp[u][0] = w[u];
return;
}
bool first = 1;
for(int i = 0; i < G[u].size(); ++i) {
int v = G[u][i];
if(v == p)
continue;
dfs(v, u);
if(first) {
for(int j = 0; j + 1 <= k; ++j)
dp[u][j + 1] = dp[v][j];
dp[u][0] = w[u] + dp[v][k];
first = 0;
} else {
for(int j = 0; j <= k; ++j) {
for(int t = k - j; t <= k; ++t)
dp[u][min(j + 1, t)] = max(dp[u][min(j + 1, t)], dp[u][t] + dp[v][j]);
}
}
}
/*for(int j = 0; j <= k; ++j)
printf("dp[%d][%d]=%d\n", u, j, dp[u][j]);*/
}
void test_case() {
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; ++i)
scanf("%d", &w[i]);
if(n == 1) {
printf("%d\n", w[1]);
return;
}
for(int i = 1; i <= n - 1; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1, -1);
int ans = 0;
for(int j = 0; j <= k; ++j)
ans = max(ans, dp[1][j]);
printf("%d\n", ans);
}
上面错在合并子树的时候可能同一棵子树重复更新了根节点的某个距离,导致状态与实际不符,解决的办法是往临时数组中更新,然后再把临时数组写回去(类似滚动数组)。而且状态设为dp[i][j]为以i为根的子树最近一个被选择点的距离超过j的最大值:
int n, k;
int w[205];
int dp[205][205];
int tmp[205];
vector<int>G[205];
void dfs(int u, int p) {
if(G[u].size() == 1 && p != -1) {
dp[u][0] = w[u];
return;
}
bool first = 1;
for(int i = 0; i < G[u].size(); ++i) {
int v = G[u][i];
if(v == p)
continue;
dfs(v, u);
if(first) {
dp[u][0] = w[u] + dp[v][k];
for(int j = 0; j <= k; ++j)
dp[u][j + 1] = dp[v][j];
for(int j = k; j >= 0; --j)
dp[u][j] = max(dp[u][j + 1], dp[u][j]);
first = 0;
} else {
tmp[0] = dp[u][0] + dp[v][k];
for(int j = 0; j <= k; ++j)
tmp[j + 1] = dp[v][j];
for(int j = 0; j <= k; ++j) {
for(int t = k - j; t <= k + 1; ++t)
tmp[min(j + 1, t)] = max(tmp[min(j + 1, t)], dp[u][t] + dp[v][j]);
}
for(int j = k; j >= 0; --j)
tmp[j] = max(tmp[j + 1], tmp[j]);
for(int j = 0; j <= k + 1; ++j)
dp[u][j] = max(dp[u][j], tmp[j]);
}
}
/*for(int j = 0; j <= k; ++j)
printf("dp[%d][%d]=%d\n", u, j, dp[u][j]);
puts("");*/
}
void test_case() {
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; ++i)
scanf("%d", &w[i]);
if(n == 1) {
printf("%d\n", w[1]);
return;
}
for(int i = 1; i <= n - 1; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1, -1);
int ans = 0;
for(int j = 0; j <= k + 1; ++j)
ans = max(ans, dp[1][j]);
printf("%d\n", ans);
}
假如设为恰好等于的话是这样的,也是要用k+1表示大于等于k的所有值,在这里截断。和南京区域赛的那个银牌题差不多。
int n, k;
int w[205];
int dp[205][205];
int tmp[205];
vector<int>G[205];
void dfs(int u, int p) {
if(G[u].size() == 1 && p != -1) {
dp[u][0] = w[u];
return;
}
bool first = 1;
for(int i = 0; i < G[u].size(); ++i) {
int v = G[u][i];
if(v == p)
continue;
dfs(v, u);
if(first) {
dp[u][0] = w[u] + max(dp[v][k], dp[v][k + 1]);
for(int j = 0; j <= k - 1; ++j)
dp[u][j + 1] = dp[v][j];
dp[u][k + 1] = max(dp[v][k], dp[v][k + 1]);
first = 0;
} else {
tmp[0] = dp[u][0] + max(dp[v][k], dp[v][k + 1]);
for(int j = 0; j <= k - 1; ++j)
tmp[j + 1] = dp[v][j];
tmp[k + 1] = max(dp[v][k], dp[v][k + 1]);
for(int j = 0; j <= k + 1; ++j) {
for(int t = max(0, k - j); t <= k + 1; ++t)
tmp[min(j + 1, t)] = max(tmp[min(j + 1, t)], dp[u][t] + dp[v][j]);
}
for(int j = 0; j <= k + 1; ++j)
dp[u][j] = max(dp[u][j], tmp[j]);
}
}
/*for(int j = 0; j <= k; ++j)
printf("dp[%d][%d]=%d\n", u, j, dp[u][j]);
puts("");*/
}
void test_case() {
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; ++i)
scanf("%d", &w[i]);
if(n == 1) {
printf("%d\n", w[1]);
return;
}
for(int i = 1; i <= n - 1; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1, -1);
int ans = 0;
for(int j = 0; j <= k + 1; ++j)
ans = max(ans, dp[1][j]);
printf("%d\n", ans);
}
不要漏掉了j=k+1状态的转移。也就是一开始设状态的时候就要考虑到有个k+1表示距离>=k+1的点。
总结:一题很好玩的树形dp。注意:1、树上度为1的虽然都是叶子,但是假如是根的话也要继续往下走。2、不要在原地更新,要另外开一个tmp数组保存。3、即使设状态为dp[i][j]为以i为根的子树最近一个被选择点的距离超过j的最大值,在合并子树的时候也要注意虽然选择t的时候是选择高的位置更新会得到更大的答案,但事实上j可以更新到一片连续的t。