Codeforces Round 1099 (Div. 2) 个人题解
Codeforces Round 1099 (Div. 2) 题解
其实感觉是思维难度不是很高的一场比赛,但是成功被 D 题单防掉大分。
A. Construct an Array
构造长度为 \(n\) 的数组 \(a_1,\dots,a_n\),满足 \(1\le a_i\le 2n\),且 \(n\) 个元素与 \(n-1\) 个相邻和共 \(2n-1\) 个数两两不同。
比较显然,选择递增的奇数构造 \(a\),则相邻两项之和为递增的偶数,不会重复。
int main(){
int Test;scanf("%d",&Test);
while(Test--){
int n;scanf("%d",&n);
for(int i=1;i<=2*n;++i){
if(i&1){
printf("%d ",i);
}
}puts("");
}
return 0;
}
B. Another Sorting Problem
给定数组 \(a\),可至多一次操作:选正整数 \(k\) 与一个子序列,给其中每个元素加 \(k\)。问能否使数组非递减。
赛时做法
使用一个神秘动态规划,定义 \(dp_{i,0/1}\) 表示第 \(i\) 个数不被/被加 \(k\) 且 \(1\sim i\) 所有数非递减的 \(k\) 的可能取值范围。转移见代码
typedef array<ll,2>ttfa;
const int N=500005;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n;
ll a[N];
array<ll,2>dp[N][2];
ttfa merge(ttfa x,ttfa y){
ttfa tmp;
tmp[0]=min(x[0],y[0]);
tmp[1]=max(x[1],y[1]);
return tmp;
}
ttfa cap(ttfa x,ttfa y){
ttfa tmp;
tmp[0]=max(x[0],y[0]);
tmp[1]=min(x[1],y[1]);
return tmp;
}
int main(){
int Test;scanf("%d",&Test);
while(Test--){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%lld",&a[i]);
}
dp[1][1]={0,INF};
dp[1][0]={0,INF};
for(int i=2;i<=n;++i){
if(a[i]>=a[i-1]){
dp[i][1]=merge(dp[i-1][1],dp[i-1][0]);
ttfa tmp={dp[i-1][1][0],min(dp[i-1][1][1],a[i]-a[i-1])};
dp[i][0]=merge(dp[i-1][0],tmp);
}else{
dp[i][0]={INF,0};
dp[i][1]={max(dp[i-1][0][0],a[i-1]-a[i]),dp[i-1][0][1]};
}
}
if(dp[n][0][0]<=dp[n][0][1]||dp[n][1][0]<=dp[n][1][1])
puts("YES");
else puts("NO");
}
return 0;
}
简单做法
显然答案的 \(k\geq \max_{i=2}^{n}a_{i-1}-a_{i}\),否则相邻递减两项就无法满足。接着 \(k=\max_{i=2}^{n}a_{i-1}-a_i\),因为如果 \(k\) 更大,更可能会导致原先不降的两项递减。
接着从前往后模拟判断即可。
int n;ll a[N];
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%d",&n);ll k=0;
for(int i=1;i<=n;++i){
scanf("%lld",&a[i]);
if(i>1)k=max(k,a[i-1]-a[i]);
}
bool ans=1;
for(int i=2;i<=n&&ans;++i){
if(a[i]<a[i-1])a[i]+=k;
if(a[i]<a[i-1])ans=0;
}
puts(ans?"YES":"NO");
}
return 0;
}
C. Chipmunk Theo and Equality
给定长为 \(n\) 的正整数序列 \(a\)。一次操作:选一个元素,若为偶数则除以 \(2\),若为奇数则加 \(1\)。求使所有元素相等所需的最少操作次数。\(1\leq a_i\leq 10^9,n\leq 10^5\)
一个数最后经过 $\log $ 级别次运算最终会变成 \(1\),进入 \(1\to 2\to1 \to 2\to 1\cdots\) 的循环。
于是至多出现 \(n\log a\) 种数,对于每种数,可以在枚举 \(a_i\) 的过程中统计多少个 \(a\) 中元素在操作过程中变成他,以及操作次数。最后统计 \(n\) 个元素均可变成的数中,操作次数和最小的即可。
第一次使用 std::map 直接超时了,最后老老实实离散化了。
int n,a[N];
int lis[N*36],tot,cnt[N*36];ll sum[N*36];
int bot[5];
int main(){
int Test;scanf("%d",&Test);
while(Test--){
tot=0;
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
}
for(int i=1;i<=n;++i){
bot[1]=bot[2]=0;
int x=a[i],num=0;
while(x>2||!bot[x]){
if(x<=2)bot[x]=1;
lis[++tot]=x;
if(x&1)++x;
else x/=2;
}
}
sort(lis+1,lis+1+tot);
tot=unique(lis+1,lis+1+tot)-lis-1;
for(int i=1;i<=n;++i){
bot[1]=bot[2]=0;
int x=a[i],num=0;
while(x>2||!bot[x]){
if(x<=2)bot[x]=1;
int loc=lower_bound(lis+1,lis+1+tot,x)-lis;
cnt[loc]++;
sum[loc]+=num;
if(x&1)++x;
else x/=2;
++num;
}
}
ll ans=INF;
for(int i=1;i<=tot;++i){
if(cnt[i]==n){
ans=min(ans,sum[i]);
}
cnt[i]=sum[i]=0;
}
printf("%lld\n",ans);
}
return 0;
}
D. Maximum Prefix Sums
给定 \(n\),二进制串 \(s\),部分已知的数组 \(a\)(若 \(s_i=1\) 则已知 \(a_i\) 且 \(|a_i|\le 10^6\),否则未知),以及数组 \(c\)(\(|c_i|\le 2\cdot10^{11}\))。定义前缀和 \(b_i=\sum_{j=1}^i a_j\) 及前缀最大值 \(c_i=\max(b_1,\dots,b_i)\)。判断能否补充未知的 \(a_i\) 使等式成立。若能,输出任意满足条件的 \(a\)(\(|a_i|\le 10^{18}\))。\(n\) 总和 \(\le 2\cdot10^5\)。
显然 \(c\) 数组是单调不降的。然后对于连续的一串 \(c_{l},c_{l+1},\dots,c_r\) 相等,会有 \(b_{l+1},b_{l+2},\cdots,b_r\leq b_l<b_{r+1}=c_{r+1}\)。
于是我们的目的就是,求一组 \(a_{l+1},a_{l+2},\dots,a_{r+1}\),满足 \(\sum_{i=l+1}^{r+1}a_i=c_{r+1}-c_{l}\) 且 \(\forall j\in[l+1,r],\sum_{i=l+1}^{j} a_i\leq 0\)。
如果我们 \([l+1,r+1]\) 中只有至多一个可自由分配的 \(a\),那么其实他的值已经被 \(c_{r+1}-c_l\) 的值定死了,这种情况直接无脑判断即可。对于其余情况我们应该让越靠前可自由分配的 \(a\) 尽可能小,由因为最后总和是确定的,我们给最靠后(可自由分配的)的元素一个超大值,给最靠前的元素一个超小值,中间全部赋值 \(0\),使得加和为 \(c_{r+1}-c_l\) 即可。可以发现这样至少能保证最靠前和最靠后 \(s_i=0\) 的位置之间的前缀和小于 \(0\),已经是最优的了。
const int N=500005;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n;
char s[N];int cnt[N];
ll a[N],c[N],sum[N],b[N];
bool check(int l,int r){
if(cnt[r]-cnt[l]==r-l){
b[l]=c[l];
for(int i=l+1;i<=r;++i){
b[i]=b[i-1]+a[i];
if(b[i]>c[l])return 0;
}
if(s[r+1]=='1'){
b[r+1]=b[r]+a[r+1];
if(b[r+1]!=c[r+1])return 0;
return 1;
}else{
a[r+1]=c[r+1]-b[r];
return 1;
}
}
b[l]=c[l];
if(s[r+1]=='0'){
a[r+1]=+1e13;
}
ll tmp=0,del=0;
for(int i=l+1;i<=r+1;++i){
tmp+=a[i];
}
del=c[r+1]-c[l]-tmp;
if(cnt[r]-cnt[l]==r-l-1){
for(int i=l+1;i<=r;++i){
if(s[i]=='0')a[i]=del;
}
}else{
for(int i=r;i>=l+1;--i){
if(s[i]=='0'){
a[i]=del+(ll)1e13;
break;
}
}
for(int i=l+1;i<=r;++i){
if(s[i]=='0'){
a[i]=-1e13;
break;
}
}
}
for(int i=l+1;i<=r;++i){
b[i]=b[i-1]+a[i];
if(b[i]>c[l])return 0;
}
return 1;
}
ll bb[N],cc[N];
int main(){
int Test;scanf("%d",&Test);
while(Test--){
scanf("%d",&n);
scanf("%s",s+1);
cnt[0]=1;s[0]='1';
for(int i=1;i<=n;++i){
cnt[i]=cnt[i-1];
if(s[i]=='1')++cnt[i];
}
bool flag=1;
for(int i=1;i<=n;++i){
scanf("%lld",&a[i]);
sum[i]=sum[i-1]+a[i];
}
for(int i=1;i<=n;++i){
scanf("%lld",&c[i]);
if(i>1&&c[i]<c[i-1])flag=0;
}
if(!flag){puts("No");continue;}
flag=check(0,0);
int l=1,r=0;
for(;l<=n&&flag;l=r+1){
while(r+1<=n&&c[r+1]==c[l])++r;
if(r==n){
int tag=0;b[l]=c[l];
for(int i=l+1;i<=r;++i){
if(s[i]=='0'){
if(!tag)a[i]=-1e13,tag=1;
else a[i]=0;
}
b[i]=b[i-1]+a[i];
if(b[i]>c[l])flag=0;
}
break;
}
flag=(flag&check(l,r));
}
if(flag){
cc[0]=-INF;
for(int i=1;i<=n;++i){
bb[i]=bb[i-1]+a[i];
cc[i]=max(cc[i-1],bb[i]);
if(cc[i]!=c[i])assert(0);
}
puts("Yes");
for(int i=1;i<=n;++i)
printf("%lld ",a[i]);
puts("");
}
else puts("No");
}
return 0;
}
赛时一直以为是写错了,但实际上只是漏 case 了。
E. Graph Cutting
给定 \(n\) 个点的树,选三个不同顶点 \(a<b<c\),取包含三点的最小连通子图(顶点数记为 \(|V|\))。求 \(|V|=d\) 的三元组个数。\(3\le d\le n\le 2000\),\(\sum n\le 2000\)。
如果要是熟知树形背包的复杂度是 \(O(n^2)\) 的那就比较好做了。
定义从点 \(u\) 的子树中选取一个点作为顶点(含 \(u\)),这个连通子图在该子树中的节点数为 \(i\) 的方案数为 \(dp1_{u,i}\)。
定义从点 \(u\) 的子树中选取两个点作为顶点(含 \(u\)),这个联通子图在该子树中的节点数为 \(i\) 的方案数为 \(dp2_{u,i}\)。
则现在已经统计了 \(u\) 的一部分子树,先需要统计 \(v\) 作为一个 \(u\) 的尚未统计的子树,与 \(u\) 已有子树和 \(u\) 节点构成的答案,为
同时更新 \(u\) 的状态
注意转移顺序即可。思路理清楚后代码很简单。
ll dp1[N][N],dp2[N][N],ans=0;
int n,siz[N],k;
vector<int>tar[N];
void dfsp(int u,int f){
siz[u]=1;
dp1[u][1]=1;
for(auto v:tar[u]){
if(v==f)continue;
dfsp(v,u);
for(int i=siz[u];i>=1;--i){
for(int j=siz[v];j>=1;--j){
dp2[u][i+j]+=dp1[u][i]*dp1[v][j];
if(i+j==k)ans+=dp2[u][i]*dp1[v][j]+dp1[u][i]*dp2[v][j];
}
}
siz[u]+=siz[v];
for(int i=1;i<=siz[v];++i){
dp1[u][i+1]+=dp1[v][i];
dp2[u][i+1]+=dp2[v][i];
}
}
}
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&k);
ans=0;
for(int i=0;i<=n;++i){
tar[i].clear();
for(int j=0;j<=n;++j)
dp1[i][j]=dp2[i][j]=0;
}
for(int i=1;i<n;++i){
int u,v;scanf("%d%d",&u,&v);
tar[u].push_back(v);
tar[v].push_back(u);
}
dfsp(1,0);
printf("%lld\n",ans);
}
return 0;
}
F. Quadratic Jumps
\(n\) 个点,\(i\) 与 \(j\) 有边当且仅当 \(|j-i|\) 为完全平方数。\(q\) 次查询 \((a,b)\),求 \(a,b\) 间最短路。\(n\) 总和 \(\le 2\times10^5\),\(q\) 总和 \(\le 10^5\)。
不知道你们怎么觉得,我是认为看成一个序列上可以向左或向右跳一个完全平方数的长度但是不能跳出 \([1,n]\) 的范围,然后求 \(a\) 到 \(b\) 的最小跳跃次数。
假定只能向一个方向跳,\(O(n\sqrt n)\) 打表可以发现一个距离 \(d\) 至多可以表示成四个完全平方数之和(Lagrange's four-square theorem)。于是答案不会超过 \(4\)。
设 \(f(d)\) 为向一个方向至少需要几个完全平方数可以移动 \(d\) 的距离。为了方便后续分类讨论,不妨 \(a<b\) 且 \(a<n-b\),意思就是如果要跳出 \([a,b]\) 范围,优先往 \([b+1,n]\) 跳,因为这个区间更大。
- 若 \(f(b-a)\leq 2\),则已经答案最优
- 若 \(f(b-a)=3\),想要在 \(2\) 步内完成,只有可能从 \(a\xrightarrow{右} [b+1,n]\xrightarrow{左} b\)。其中无论是哪一步,都只需枚举 \(\sqrt n\) 种长度。
- 若 \(f(b-a)=4\),同样可以使用上面的方法先判断是否可以在两步内完成。现在想要在 \(3\) 步内完成,只有可能 \(2\) 步同方向,\(1\) 步反方向,我们枚举 \(\sqrt{n}\) 种反方向的距离(以及方向也要考虑在内),则另外两步的距离和是已知的,这两步距离和的组成也可以预处理出来(这部分预处理算出来对于同一个距离不超过 \(12\) 种),判断能否满足边界条件即可。
总实现复杂度 \(O(n(\sqrt n+\sqrt q))\),且还有至多 \(12\) 的一个可看作常数的东西(其实是我不会分析)。
分类讨论具体见代码注释,其中数字 \(x\) 表示第 \(x\) 步后跳到的相对位置。
const int N=500005;
const ll INF=0x3f3f3f3f3f3f3f3f;
int f[N];
vector<ttfa>lis[N];
void init(int R=500000){
memset(f,0x3f,sizeof(f));
f[0]=0;
for(int i=1;i<=R;++i){
for(int j=1;i-j*j>=0;++j){
f[i]=min(f[i],f[i-j*j]+1);
}
}
for(int i=1,x=i*i;x<=R;++i,x=i*i){
for(int j=i,y=j*j;x+y<=R;++j,y=j*j){
lis[x+y].push_back({x,y});
}
}
// int maxv=0;
// for(int i=1;i<=R;++i)
// maxv=max(maxv,(int)lis[i].size());
//printf("%d\n",maxv);
}
int n,q;
int solve(int d,int l,int r){
if(l>r)swap(l,r);
if(f[d]==1||f[d]==2)return f[d];
int ans=f[d];
for(int i=1,x=i*i;x<=n;++i,x=i*i){
if(x<=r){//0 2 1 或 0 3 2 1
ans=min(ans,f[d+x]+1);
}else{
for(auto [y,z]:lis[x+d]){// 2 0 3 1
if(d<=z&&z<=d+r&&x-z<=l)
ans=min(ans,3);
}
}
if(x<=d){
if(f[d]==4){
for(auto [y,z]:lis[x+d]){//0 2 1 3
if(y<=d&&y>=x)
ans=min(ans,3);
}
}
}
if(x>=d){
if(x<=d+r)ans=min(ans,1+f[x-d]);
if(f[d]==4){
for(auto [y,z]:lis[x-d]){//1 0 3 2
if(y<=l&&z<=r)
ans=min(ans,3);
}
}
}
}
return ans;
}
int main(){
init();
int Test;scanf("%d",&Test);
while(Test--){
scanf("%d%d",&n,&q);
int l,r;
for(int T=1;T<=q;++T){
scanf("%d%d",&l,&r);
printf("%d\n",solve(r-l,l-1,n-r));
}
}
return 0;
}
感觉就是打个表什么都出来了(虽然我疯狂漏 case)

浙公网安备 33010602011771号