前言
没错,这场打得稀烂,A 题一个小时都没切,掉下浅绿名......
Standings:7955
题目链接:Dashboard - Codeforces Round 958 (Div. 2) - Codeforces
A. Split the Multiset
我愿称之为目前做过的最难的 A 题!(其实 A 能难过 BCD 吗?可能是比赛的时候短路了一直往错的贪心想)
题意:
给一个只含有一个元素 n 的多重集合,需要你进行若干次操作,每次操作选择一个集合里的一个元素 u ,再向集合里加入不超过 k 个元素,使这些加入的元素之和等于 u 。最后要求得到的多重集合是 n 个 1 ,求最小的操作次数。
思路:
一眼贪心,很像合并果子之类的题目,首先显然的贪心是当 u >= k 的时候,一定要加满 k 个元素,这样不会导致浪费操作,下面考虑加入的这些元素要如何分配。其实只需尽可能多地加入“1” 就是最优方案。
第一种解释:我们最后要得到的全为 “1” 的多重集合,这样可以保证我们每一次操作得到最多的 “1” ,可以让我们以最快的速度达成目标。
第二种解释:无论如何分配这不超过 k 个元素,要减少的总数值都是(n - 1),分配尽可能多的 “1” 是无须进行二次操作的,而如果分配的数里有两个或以上不为 1 的数,这些数需要单独二次操作,这样每个数二次操作可能用不满 k 个元素,这样就导致了更多的操作数浪费。
#include<cstdio>
#include<cstring>
using namespace std;
int T,n,k;
int ask(int x,int y)
{
if(x == 1) return 0;
if(x <= y) return 1;
return ask(x - (k - 1),y) + 1;
}
int main()
{
scanf("%d",&T);
while (T --)
{
scanf("%d%d",&n,&k);
printf("%d\n",ask(n,k));
}
return 0;
}
B. Make Majority
题意:
给一个长度为 n 的 01 序列 a,每次操作选取一段区间 [l,r] ,记 表示这段区间内 0 的数目,记
表示这段区间内 1 的数目。
1. >=
,则将这段区间变成单个元素 0
2. <
,则将这段区间变成单个元素 1
询问对于每个给定的 01 串,能否通过若干次操作使最终的序列全是 1 。
思路:
简单来说就是要消灭所有的 0 ,显然我们可以先把所有长度大于 1 的连续的 0 都缩成一个单个的 0 ,那么此时原数列就变成了若干个单个的 0 嵌在一堆 1 中间,下面我们考虑对这些 0 逐个击破。对于每个 0 ,消灭它需要身边有至少两个 1 (“011”、“101”、“110”),每次我们选取这样连续的三个数并把它变成 1 ,发现每次操作都是减少了一个 1 和一个 0 。于是我们只需判断处理了连续 0 之后的序列中, >=
+ 1 是否成立即可。若成立,我们每次都一定可以找到至少一个这样的三个数并击破,最后得到全为 1 的串。
#include<cstdio>
#include<cstring>
using namespace std;
#define N 200005
int T,n,a[N];
char s[N];
int main()
{
scanf("%d",&T);
while (T --)
{
scanf("%d",&n);
scanf("%s",s + 1);
for (int i = 1;i <= n;++ i) a[i] = s[i] - '0';
int x,y;
x = y = 0;
if(a[1] == 1) x = 1;
else y = 1;
for (int i = 2;i <= n;++ i)
if(a[i] != a[i - 1])
{
if(a[i] == 1) ++ x;
else ++ y;
}
else if(a[i] == 1) ++ x;
if(x >= y + 1) printf("Yes\n");
else printf("No\n");
}
return 0;
}
C. Increasing Sequence with Fixed OR
题意:
给定一个正整数 n ,构造出一个满足以下条件的最长序列。
1. 2<=i<=k ,
<= n
2. 序列 a 严格递增
3. 2<=i<=k ,
= n
思路:
我们从大到小构造序列,那么显然最大的那个数可以构造为 n ,考虑往下如何构造。
由于要使相邻两个数异或和为 n ,我们又是从大到小构造的,于是每一步的最优方案就是 n 从小到大依次减去二进制下的 1 。
我们以 23 为例,在二进制下观察:
1 0 1 1 1 (23)
1 0 1 1 0 (22)
1 0 1 0 1 (21)
1 0 0 1 1 (19)
0 0 1 1 1 (7)
因此假设 n 的二进制下有 m 个 1 ,则最长序列的长度为 m + 1 。
#include<cstdio>
#include<cstring>
using namespace std;
int T,m;
long long pow[62],n,a[62];
int main()
{
pow[0] = 1ll;
for (int i = 1;i <= 61;++ i) pow[i] = pow[i - 1] << 1;
scanf("%d",&T);
while (T --)
{
scanf("%lld",&n),m = 0;
long long now = n;
for (int i = 61;i >= 0;-- i)
if(now >= pow[i]) a[++ m] = pow[i],now -= pow[i];
if(m == 1)
{
printf("1\n");
printf("%lld\n",n);
continue;
}
printf("%d\n",m + 1);
for (int i = 1;i <= m;++ i) printf("%lld ",n - a[i]);
printf("%lld\n",n);
}
return 0;
}
D. The Omnipotent Monster Killer
题意:
给一棵 n 个节点的树,每个节点上都有一个怪兽,每个怪兽都有一个攻击值 。每一轮所有存活的怪兽都会对你产生攻击,之后你需要在每一轮消灭若干怪兽,被消灭的怪兽在树上不能相邻,直到所有的怪兽都被消灭,求这个过程中怪兽产生的最小攻击值总和。
思路:
一眼树形dp,而且很像最经典的那道题 —— “ 没有上司的舞会 ” 。考场时飞速码完了但一直 WA,造成了这次 rating 的惨剧。
设 表示节点 i 上的怪兽在第 j 轮被消灭时,以 i 为根的子树内总攻击值的最小值,那么它本身的贡献就是
* j ,在转移的时候从子节点上的怪兽都不在第 j 轮消灭来转移即可。
时间复杂度:不用前缀和优化就是 O( ),前缀和优化就是 O(nlogn)。
#include<cstdio>
#include<cstring>
using namespace std;
#define N 300005
#define Inf 8e18
int T,n,st[N],cnt;
long long a[N],f[N][22],ans,pre[N][22],suf[N][22];
struct Edge
{
int next,to;
}ed[N << 1];
long long min(long long x,long long y) { return x < y ? x : y ; }
void add(int u,int v)
{
ed[++ cnt].next = st[u];
ed[cnt].to = v;
st[u] = cnt;
return;
}
void dfs(int x,int fa)
{
for (int i = 1;i <= 20;++ i) f[x][i] = 1ll * i * a[x];
for (int i = st[x]; ~i ;i = ed[i].next)
{
int rec = ed[i].to;
if(rec == fa) continue;
dfs(rec,x);
for (int j = 1;j <= 20;++ j) pre[rec][j] = min(pre[rec][j - 1],f[rec][j]);
for (int j = 20; j ;-- j) suf[rec][j] = min(suf[rec][j + 1],f[rec][j]);
for (int j = 1;j <= 20;++ j) f[x][j] += min(pre[rec][j - 1],suf[rec][j + 1]);
}
return;
}
int main()
{
scanf("%d",&T);
while (T --)
{
scanf("%d",&n),cnt = 0,ans = Inf;
for (int i = 1;i <= n;++ i)
{
scanf("%lld",&a[i]),st[i] = -1;
for (int j = 0;j <= 21;++ j) f[i][j] = pre[i][j] = suf[i][j] = Inf;
}
for (int i = 1,u,v;i < n;++ i) scanf("%d%d",&u,&v),add(u,v),add(v,u);
dfs(1,0);
for (int i = 1;i <= 20;++ i) ans = min(ans,f[1][i]);
printf("%lld\n",ans);
}
return 0;
}
E - Range Minimum Sum
一道计数好题。
题意:
给一个长度为 n 的序列a,定义 ,对于每个 i ,单独解决以下问题:
从序列 a 中移除 ,剩下的数记作序列 b ,计算 f(b) 。
思路:
我们首先计算一个数都不删减时的答案,再考虑删除每个数之后产生的影响。
一个数都不删的情况:
统计出每个位置左右第一个小于当前这个数的位置,分别记作 和
,那么这个位置上的贡献就是( i -
)* (
- i ) *
,求 l,r 数组的过程可以用单调栈线性解决。
删除位置i上的数对答案产生的影响:
1. 首先显然要减去( i - )* (
- i ) *
2. 假设除去 i 之外的某个位置 j ,区间 [ + 1 ,
- 1 ] 覆盖了位置i,那么在删去 i 之后, j 的贡献也会减少。如果 i 位于左侧区间 [
+ 1 , j - 1 ],那么少了一个 i 对于 j 的贡献的影响就是减少 1 个 (
- i ) *
。i 位于右侧区间的情况同理。那么这段贡献的变化如何统计呢?我们可以枚举 j ,那么对于 [
+ 1 , j - 1 ] 和 [ j + 1 ,
- 1 ] 区间内的每个 i ,删除 i 都会造成上述影响,这实际上是两段区间减,用差分数组维护即可。
3. 对于某个位置 j ,若 或者
刚好等于 i,那么在删除i之后就会更新 j 的
或者
,更新之后 j 的贡献会增加。更新 l,r 的过程即在 i 的左右找出第一个比
小的数,这部分可以用二分 + ST表解决(如果用二分 + 线段树就会带两个 log ,可能超时)。
时间复杂度:O(n log n)
真是一道酣畅淋漓的计数好题!
#include<cstdio>
#include<cstring>
#include<stack>
using namespace std;
#define N 500005
int T,n,a[N],l[N],r[N],lg[N],f[N][22];
long long tot,ans[N],dif[N];
stack<int> st;
int min(int x,int y) { return x < y ? x : y ; }
void solveLR()
{
while (!st.empty()) st.pop();
for (int i = 1;i <= n + 1;++ i)
{
while (!st.empty() && a[i] < a[st.top()]) r[st.top()] = i,st.pop();
st.push(i);
}
while (!st.empty()) st.pop();
for (int i = n;i >= 0;-- i)
{
while (!st.empty() && a[i] < a[st.top()]) l[st.top()] = i,st.pop();
st.push(i);
}
return;
}
void solveST()
{
for (int j = 1;j <= lg[n + 1];++ j)
for (int i = 0;i + (1 << j) - 1 <= n + 1;++ i)
f[i][j] = min(f[i][j - 1],f[i + (1 << (j - 1))][j - 1]);
return;
}
int findL(int x,int w)
{
int ll = 0;
int rr = x - 1;
int tmp = 0;
while (ll <= rr)
{
int mid = (ll + rr) >> 1;
int len = x - mid;
if(min(f[mid][lg[len]],f[x - (1 << lg[len])][lg[len]]) < w) tmp = mid,ll = mid + 1;
else rr = mid - 1;
}
return tmp;
}
int findR(int x,int w)
{
int ll = x + 1;
int rr = n + 1;
int tmp = n + 1;
while (ll <= rr)
{
int mid = (ll + rr) >> 1;
int len = mid - x;
if(min(f[x + 1][lg[len]],f[mid - (1 << lg[len]) + 1][lg[len]]) < w) tmp = mid,rr = mid - 1;
else ll = mid + 1;
}
return tmp;
}
int main()
{
lg[1] = 0;
for (int i = 2;i < N;++ i) lg[i] = lg[i >> 1] + 1;
scanf("%d",&T);
while (T --)
{
scanf("%d",&n),a[0] = a[n + 1] = 0,tot = dif[0] = 0ll;
for (int i = 1;i <= n;++ i) scanf("%d",&a[i]),dif[i] = 0ll,f[i][0] = a[i];
solveLR();
solveST();
for (int i = 1;i <= n;++ i) tot += 1ll * (i - l[i]) * (r[i] - i) * a[i];
for (int i = 1;i <= n;++ i)
{
ans[i] = tot - 1ll * (i - l[i]) * (r[i] - i) * a[i];
dif[l[i] + 1] -= 1ll * (r[i] - i) * a[i],dif[i] += 1ll * (r[i] - i) * a[i];
dif[i + 1] -= 1ll * (i - l[i]) * a[i],dif[r[i]] += 1ll * (i - l[i]) * a[i];
}
for (int i = 1,nl,nr;i <= n;++ i)
{
nl = findL(l[i],a[i]);
if(l[i]) ans[l[i]] += 1ll * (l[i] - nl - 1) * (r[i] - i) * a[i];
nr = findR(r[i],a[i]);
if(r[i] != n + 1) ans[r[i]] += 1ll * (nr - r[i] - 1) * (i - l[i]) * a[i];
}
for (int i = 1;i <= n;++ i)
{
dif[i] += dif[i - 1];
ans[i] += dif[i];
printf("%lld ",ans[i]);
}
printf("\n");
}
return 0;
}
总结
事实摆在那,这场确实打得稀烂,最大的原因是 A 题滑铁卢,但本质上是思维上仍有缺口,不过好在是还有时间练习。D 题自己打的 dp 现在仍然不清楚错在哪里,可能是方法本身有点瑕疵,虽然 “ 到现在都不知道错哪了 ” 还是心里的一道坎,但多多学习正规的解法才是王道,很多时候得取其精华、去其糟粕。