AtCoder Grand Contest 041
Preface
花了两个晚自习加上一个中午和一个周末晚上和大量的自习课时间打完了
ABE还勉强算自己想的都是陈指导想的,C看了点提示在同学的帮助下Rush出来了,D只能自己想出\(O(n^3)\)暴力,F想了两天连状态都不知道怎么设看了题解直呼内行
总结:水平太次,建议回炉重造
A - Table Tennis Training
签到题,注意到如果两个人之间的位置差是偶数的话直接走就好了
否则就是两种情况:
- 在最左边或最右边相遇(先到的人可以一直等后面的人到)
- 在左边的人到了最左边之后等待一轮,然后在向右走与右边的人碰头;右边的人同理
讨论一下取最小值即可,WA了一发没看见要开long long
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
long long n,a,b;
int main()
{
scanf("%lld%lld%lld",&n,&a,&b);
if ((b-a)%2==0) return printf("%lld",(b-a)/2),0;
long long res=min(b-1,n-a); res=min(res,min((a+b)/2,(2*n-a-b+1)/2));
return printf("%lld",res),0;
}
B - Voting Judges
简单分析题。首先我们发现给\(a_i\)从大到小排序后前\(p\)个数是一定合法的(废话)
然后考虑从\(p+1\)开始枚举最小的能被加到前\(p\)的位置\(i\),贪心地想我们肯定只要加到第\(p\)个地方即可
考虑这个位置每次都被加\(1\),那么剩下的\(v-1\)次怎么分配?
显然对于所有位置在它之后以及前\(p-1\)个数都可以随便加,那么我们给这些数也加满
那么问题来了,如果操作次数还有多怎么办,只能填在\([p+1,i)\)之间了
考虑判断不合法时怎么操作,我们求出\(S=\sum_{j=p+1}^{i-1} a_j\),然后判断\((a_i+m)\times (i-p)-S\)与剩下来的增加次数的关系
为什么可以这么做呢,因为\([p+1,i)\)中的数都\(\ge a_i\),那么每个数都是可以经过\(m\)次操作超过\(a_i+m\)的,那么极限情况就是全加到\(a_i+m\)
时间复杂度\(O(n\log n)\),因为要排序。然后有一个坑点,要判断\(a_i+m\ge a_p\),刚开始石乐志没判WA了两发
#include<cstdio>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int n,a[N],m,v,p; long long sum[N];
inline bool cmp(CI x,CI y)
{
return x>y;
}
int main()
{
RI i; for (scanf("%d%d%d%d",&n,&m,&v,&p),i=1;i<=n;++i) scanf("%d",&a[i]);
for (sort(a+1,a+n+1,cmp),i=1;i<=n;++i) sum[i]=sum[i-1]+a[i];
for (i=p+1;i<=n;++i)
{
int dlt=max(0,v-(p+n-i)); long long tot=sum[i-1]-sum[p-1];
if (a[i]+m<a[p]||1LL*(i-p)*(a[i]+m)+1-tot<=1LL*dlt*m) return printf("%d",i-1),0;
}
return printf("%d",n),0;
}
C - Domino Quality
手玩构造题。这种题目看完就想着先手玩小的看看规律
然后和陈指导Rush了快1hour然后只搞出了\(n=3,4,5\)的情况,感觉没什么规律
瞄了一眼题解,原来\(n\)是偶数可以构造,回班之后开始尝试,竟然很快就Rush出来了(自习课加智商),以\(n=8\)为例(一看就知道怎么构造的说):
aa....cd
bb....cd
cdaa....
cdbb....
..cdaa..
..cdbb..
....cdaa
....cdbb
同时也容易想到\(n|3\)时可以直接用\(3\times 3\)的拼出来,那么现在就考虑\(n \not |\ 3\)的奇数,考虑因为我们偶数的构造法行列都是三个,然后\(n=5\)时的答案:
aabc.
..bcd
fee.d
f.ghh
iigjj
也是行列三个,容易想到我们先把左上角填一个\(5\times 5\)的,然后在右下角填一个\((n-5)\times (n-5)\)的
然后大功告成?发现当\(n=7\)的时候就GG了,那怎么办?
手玩构造\(n=7\)的答案(感谢无私奉献的同桌):
ab...c.
ab...c.
..dee.f
..d.g.f
..hhg.i
jj..kki
llmm.nn
然后终于做完了,WA了一发在拼图的时候把偶数的最后一块填到第一行去了。。。
#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005;
char
tp5[N][N]={
"aabc.",
"..bcd",
"fee.d",
"f.ghh",
"iigjj"
},
tp7[N][N]={
"ab...c.",
"ab...c.",
"..dee.f",
"..d.g.f",
"..hhg.i",
"jj..kki",
"llmm.nn"
};
int n; char a[N][N];
inline void odd_paint(CI sx=0)
{
for (RI i=sx;i<n;i+=2)
a[i][i]=a[i][i+1]='a',a[i+1>=n?i+1-n+sx:i+1][i]=a[i+1>=n?i+1-n+sx:i+1][i+1]='b',
a[i+2>=n?i+2-n+sx:i+2][i]=a[i+3>=n?i+3-n+sx:i+3][i]='c',a[i+2>=n?i+2-n+sx:i+2][i+1]=a[i+3>=n?i+3-n+sx:i+3][i+1]='d';
}
inline void print(CI n,char c[N][N])
{
for (RI i=0;i<n;++i)
{
for (RI j=0;j<n;++j) putchar(c[i][j]); if (i!=n-1) putchar('\n');
}
}
int main()
{
RI i,j; scanf("%d",&n); if (n==2) return puts("-1"),0;
if (n==5) return print(5,tp5),0;
if (n==7) return print(7,tp7),0;
for (i=0;i<n;++i) for (j=0;j<n;++j) a[i][j]='.';
if (n%3==0)
{
for (i=0;i<n;i+=3) for (j=0;j<n;j+=3)
a[i][j]=a[i][j+1]='a',a[i+1][j+2]=a[i+2][j+2]='b';
return print(n,a),0;
}
if (n%2==0) return odd_paint(),print(n,a),0;
for (i=0;i<5;++i) for (j=0;j<5;++j) a[i][j]=tp5[i][j];
return odd_paint(5),print(n,a),0;
}
D - Problem Scores
Rush了C之后看了看DE题面就滚回班了,连着想了两天然后只出了一个\(O(n^4)\)(然后被陈指导指点了一下复杂度发现是\(O(n^3)\)的)
首先我们考虑直接令\(a_i\)增序,然后记\(S_i=\sum_{j=1}^i a_j\),那么题中的条件等价于:
显然由于\(S_k+S_{n-k+1}\)的对称性,我们只要做一半即可,换句话说只要前\(\lfloor\frac{n+1}{2}\rfloor\)满足条件即可
然后我们考虑给\(S\)展开然后移个项,就有\(S_k+S_{n-k+1}>S_n\Leftrightarrow a_1-\sum_{i=2}^k(a_{n-i+2}-a_i)>0\)
然后我们观察这个式子,当\(k\le\lfloor\frac{n+1}{2}\rfloor\)时,\(a_{n-i+2}-a_i\ge 0\),说明左边的值单调不增
同时根据题设,\(a_{n-i+2}-a_i\)也是单调不增的,因此容易发现我们可以把两维都压到状态里
设\(f_{i,j,k}\)表示前\(i\)个数,\(a_1-\sum_{i=2}^k(a_{n-i+2}-a_i)=j\),\(a_{n-i+2}-a_i=k\)的方案数,然后转移的时候枚举上一次的差值\(t\),容易得到转移:
直接维护两个数组记录上面两个项的后缀和然后减一减即可,注意下边界
然后我们发现\(n\)是奇数的时候中间的数要特别考虑,还要在枚举转移方式讨论一下(实现详见代码)
然后这个暴力DP状态\(O(n^3)\)转移\(O(1)\),因此复杂度\(O(n^3)\)
当然如果你想伟大的陈指导一样压一下上界的话,复杂度是\(O(n^2\log n)\)的
暴力的代码:
#include<cstdio>
#define RI register int
#define CI const int&
const int N=5005;
int n,mod,f[N][N],g[N][N],c[N][N],ans; // f[x][y] x:sum y:last delta
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline int sum(CI x,CI y)
{
int t=x+y; return t>=mod?t-mod:t;
}
int main()
{
RI i,j,k; for (scanf("%d%d",&n,&mod),i=1;i<=n;++i) f[i][n-i]=1;
for (j=1;j<=n;++j) for (k=n;~k;--k) g[j][k]=sum(g[j][k+1],f[j][k]);
for (j=1;j<=n;++j) for (k=n;~k;--k) c[j][k]=sum(c[j][k+1],1LL*f[j][k]*k%mod);
for (i=2;i<=1+(n-1)/2;++i)
{
for (j=1;j<=n;++j) for (k=0;k<=n;++k) f[j][k]=0;
for (j=1;j<=n;++j) for (k=0;k<=n-1;++k) if (j+k<=n) inc(f[j][k],sum(c[j+k][k],(mod-1LL*g[j+k][k]*(k-1)%mod)%mod));
for (j=1;j<=n;++j) for (k=0;k<=n;++k) g[j][k]=c[j][k]=0;
for (j=1;j<=n;++j) for (k=n;~k;--k) g[j][k]=sum(g[j][k+1],f[j][k]);
for (j=1;j<=n;++j) for (k=n;~k;--k) c[j][k]=sum(c[j][k+1],1LL*f[j][k]*k%mod);
}
for (i=1;i<=n;++i) for (j=0;j<=n;++j) inc(ans,1LL*(n&1?1:j+1)*f[i][j]%mod);
return printf("%d",ans),0;
}
然后这样的DP肯定就是考虑如何去掉其中的一维,而我和陈指导一直都以为要去掉和的那一维,然后笋干爆炸
考虑进一步转化问题,我们设\(x_i=a_{n-i+2}-a_i\),特殊地\(x_1=n-a_1\)
然后现在问题变成了\(x_i\in[0,n),x_i\ge x_{i+1}\),求\(\prod_{i=2}^k (x_{i-1}-x_i+1)\)(结合前面DP的转移系数看一下就能发现只是把问题变了个形式)
考虑我们还是依次枚举\(x_i\),由于\(x_i\le x_j,j\in[1,i)\),我们可以把所有的\(x_j\)都减去\(x_i\)(因为不影响差值),即
容易发现现在的\(x‘_j\)和之前定义的取值范围相同了,也就是说他可以看做原问题的子问题,我们可以用一个状态一起表示
所以设\(f_{i,j}\)表示前\(i\)个\(x_i\),\(\sum_{k=1}^i x_k=j\)时\(\prod_{k=2}^i (x_{k-1}-x_k+1)\),然后我们只需要枚举当前的\(x_i\)即可转移
但是虽然DP好写了但是还是\(O(n^3)\)的啊,不过对于这个DP我们很容易优化(据说是常见技巧)
考虑\(x_{k-1}-x_k+1\)相当于在\([x_k,x_{k-1}]\)中选出一个点(姑且叫做关键点)的方案数
然后我们可以不用枚举当前的\(x_i\)的值,加一维\(0/1\)表示当前是否选择了关键点,然后就可以把许多状态同时按序转移了
具体详见代码,复杂度\(O(n^2)\)
#include<cstdio>
#define RI register int
#define CI const int&
const int N=5005;
int n,m,mod,f[N][N][2],ans; // f[x][y] x:sum y:last delta
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline int sum(CI x,CI y)
{
int t=x+y; return t>=mod?t-mod:t;
}
int main()
{
RI i,j; scanf("%d%d",&n,&mod); m=(n-1)/2;
for (f[0][0][0]=i=1;i<=m;++i) for (j=0;j<=n;++j)
{
f[i][j][1]=f[i-1][j][0];
if (i<=j) inc(f[i][j][1],f[i][j-i][1]);
f[i][j][0]=f[i][j][1];
if (i<=j) inc(f[i][j][0],f[i][j-i][0]);
}
if (n&1)
{
for (i=1;i<=n;++i) for (j=0;n-i-j*(m+1)>=0;++j)
inc(ans,f[m][n-i-j*(m+1)][0]);
} else
{
for (i=1;i<=n;++i) for (j=0;n-i-j*(m+1)>=0;++j)
inc(ans,1LL*f[m][n-i-j*(m+1)][0]*(j+1)%mod);
}
return printf("%d",ans),0;
}
E - Balancing Network
E题小清新乱搞题,感觉比D简单多了
首先考虑\(T=1\),容易想到如果我们枚举最后走到的电线\(i\)
假设每条电线都有机会走到\(i\),考虑可以从后往前处理每条路径构造答案
如果路径两端都能到\(i\)或者都到不了\(i\)显然就可以随便放,否则让路径从不能到的指向能到的即可
那么怎么判断每条电线能否被其他电线走到呢,很简单直接把路径先当成双向的然后bitset
搞一波即可
这部分就轻松做完了,复杂度\(O(\frac{nm}{w})\)
然后考虑\(T=2\),我们还是一样的从后往前处理路径,记录一个\(tar_i\)表示电线\(i\)最后走到的电线是哪条
显然如果我们把路径定向成\(x\to y\)那么\(tar_x=tar_y\)
由于要让最后所有的\(tar_i\)不全相同,那么我们尽量每次让\(tar_i\)出现次数多的变成出现次数少的,不难证明这样是可以满足要求了
然后特殊情况是\(n=2\),此时无论如何都会走到一起,特判即可,一发入魂
#include<cstdio>
#include<bitset>
#define RI register int
#define CI const int&
using namespace std;
const int N=50005;
int n,m,tp,x[N<<1],y[N<<1]; char s[N<<1];
namespace Case1
{
bitset <N> vis[N]; bool c[N];
inline void solve(void)
{
RI i,j; for (i=1;i<=n;++i) vis[i].set(i);
for (i=1;i<=m;++i) vis[x[i]]=vis[y[i]]=vis[x[i]]|vis[y[i]];
for (i=1;i<=n;++i) if (vis[i].count()==n)
{
for (c[i]=1,j=m;j;--j) if (c[x[j]]) s[j]='^',c[y[j]]=1;
else if (c[y[j]]) s[j]='v',c[x[j]]=1; else s[j]='^';
return (void)(puts(s+1));
}
puts("-1");
}
};
namespace Case2
{
int tar[N],c[N];
inline void solve(void)
{
if (n<=2) return (void)(puts("-1")); RI i;
for (i=1;i<=n;++i) c[tar[i]=i]=1; for (i=m;i;--i)
if (c[tar[x[i]]]>=c[tar[y[i]]]) s[i]='v',--c[tar[x[i]]],++c[tar[x[i]]=tar[y[i]]];
else s[i]='^',--c[tar[y[i]]],++c[tar[y[i]]=tar[x[i]]]; puts(s+1);
}
};
int main()
{
RI i; for (scanf("%d%d%d",&n,&m,&tp),i=1;i<=m;++i)
scanf("%d%d",&x[i],&y[i]); if (tp==1) Case1::solve(); else Case2::solve();
return 0;
}
F - Histogram Rooks
想的时候完全没有思路的说,ORZcz_xuyixuan聚聚
这类计数DP最难的地方就在于状态的设计,如何设计一盒好的状态不重不漏同时复杂度够低
考虑对于每一列的状态,有以下三种可能:
- 这一列有车
- 这一列没车,但是每行都被其他列的车控制了
- 这一列没车,而且存在没有被控制的行
然后考虑关键的一点,假设我们当前只考虑这一列的上面的一些行,那么如果下面的那些行中每行都被控制了,那么(2)类列可以被视为(1)类列;否则(2)类列被视为(3)类列
那么我们在状态中可以把某些列的上面\(h\)行压入状态,同时加上一维\(0/1\)表示在下面的行中是否存在没有车的行,然后把(3)类列的个数记录进去即可
转移的话由于需要逐行转移,由于这里的图高度参差不齐,因此每次可以移除最下面的一行或者断开已经空了的列处理子图
乍一看状态很多,实际上由于只会被断开\(n\)次,状态数仍然是\(O(n^3)\)级别的,转移的话讨论放这一列或者不放即可,代码中注释比较详尽
用记忆化搜索实现即可,转移的时候类似树上背包是不会影响最终的复杂度的
#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=405,mod=998244353;
int n,a[N],f[N<<1][N][N][2],pw2[N],C[N][N],id[N][N],size[N<<1],tot;
//f[i][j][k][0/1] i:state j:height k:unfilled number 0/1 is under finished?
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline int DP(CI l=1,CI r=n,CI h=0)
{
if (l>r) return f[0][h][0][0]=f[0][h][0][1]=1,0;
int &now=id[l][r],pos=l; RI i,j; if (!now) now=++tot;
for (i=l+1;i<=r;++i) if (a[i]<a[pos]) pos=i;
if (a[pos]==h) //remove the lowest column
{
int x=DP(l,pos-1,h),y=DP(pos+1,r,h);
size[now]=size[x]+size[y]+1; //size calculated the number of column
for (i=0;i<=size[x];++i) for (j=0;j<=size[y];++j)
inc(f[now][h][i+j][0],1LL*f[x][h][i][0]*f[y][h][j][0]%mod), // if draw
inc(f[now][h][i+j+1][1],1LL*f[x][h][i][1]*f[y][h][j][1]%mod); //or so
} else //remove the lowest row
{
for (DP(l,r,h+1),i=0;i<=size[now];++i)
inc(f[now][h][i][0],f[now][h+1][i][1]),inc(f[now][h][i][1],f[now][h+1][i][1]),//filled draw or not
inc(f[now][h][i][0],1LL*f[now][h+1][i][0]*(pw2[size[now]-i]-1)%mod),
//draw i columns and fill other size[now]-i columns (can't all empty)
inc(f[now][h][i][1],1LL*f[now][h+1][i][1]*(pw2[size[now]-i]-1)%mod);
for (i=1;i<=size[now];++i) for (j=1;j<=i;++j)
inc(f[now][h][i-j][0],1LL*f[now][h+1][i][0]*pw2[size[now]-i]%mod*C[i][j]%mod),
//choose j columns from i filled columns, other columns are filled optionally
inc(f[now][h][i-j][1],1LL*f[now][h+1][i][1]*pw2[size[now]-i]%mod*C[i][j]%mod);//same as above
}
return now;
}
int main()
{
RI i,j; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]);
for (pw2[0]=i=1;i<=n;++i) pw2[i]=2LL*pw2[i-1]%mod;
for (C[0][0]=i=1;i<=n;++i) for (C[i][0]=j=1;j<=n;++j)
C[i][j]=C[i-1][j-1],inc(C[i][j],C[i-1][j]);
int now=DP(); return printf("%d",f[now][0][0][0]),0;
}
Postscript
一周做一场AGC再写个题解就差不多的说,其他题目都没什么时间去写了233