CSP-S 2021 游记 & 总结
游记? 游寄!
考前几天一直在打模拟赛,把把被吊着打。
考试前在场外口胡:今年有字符串;上午普及没\(DP\),下午也没有;CSP不会考网络流;(flag)
考试前在考场外面等了很长时间,好像我们考场进的非常晚...
考试键盘非常软!差评! 考试电脑非常卡!差评!\(\mathrm{14:31}\) 才给解压密码! 差评!
T1.廊桥分配
考试的时候没多想,看了一会觉得 \(T1\) 不会这么难吧,于是猜了一下有单峰性写了个三分,于是就寄了。
正解的话,首先我们知道,对于任何一架飞机,他是否能停靠在廊桥与他后面到达机场的飞机无关。
所以有事实:若可用廊桥数为 \(k\) 时 某架飞机可以停在廊桥,那么当廊桥数大于 \(k\) 时该飞机也可以停在廊桥。
那么我们先假设有无限座廊桥,然后对每架飞机,我们要求他如果可以停靠在廊桥,就停靠在编号最小的可以停靠的廊桥。我们求出对于每架飞机他停在了第 \(f_i\) 座廊桥上。那么第 \(i\) 架飞机能停靠廊桥当且仅当廊桥数量大于等于 \(f_i\) 。
可以用优先队列模拟得出 \(f_i\) 。然后枚举怎么分配机场,取最大值即可。
具体实现时,先对飞机按照时间排序,然后离散化。把 \(f_i\) 求出来之后可以树状数组,不过已经离散化过了,所以可以前缀和。
时间复杂度:\(\mathcal{O}(n\log n)\)
Code:
int n,m1,m2;
struct node{
int a,b;
bool operator <(const node &p) const{
return a<p.a;
}
};
node g[2][DMAX];
int b[DMAX<<1],cnt=0;
int f[2][DMAX];
int tim[DMAX<<1];
int tag[DMAX<<1];
int pre[2][DMAX<<1];
int maxt1,maxt2;
priority_queue<int,vector<int>,greater<int> > q;
void solve(){
for(int i=1;i<=m1;i++){
q.push(i);
tim[g[0][i].a]=i;
tim[g[0][i].b]=i;
}
for(int i=1;i<=maxt1;i++){
if(tim[i]==0){
continue;
}
int now=tim[i];
if(!tag[now]){
int u=q.top();
q.pop();
tag[now]=u;
f[0][now]=u;
}
else{
q.push(tag[now]);
tag[now]=0;
}
}
mem(tim,0);
while(!q.empty()){
q.pop();
}
for(int i=1;i<=m2;i++){
q.push(i);
tim[g[1][i].a]=i;
tim[g[1][i].b]=i;
}
for(int i=1;i<=maxt2;i++){
if(tim[i]==0){
continue;
}
int now=tim[i];
if(!tag[now]){
int u=q.top();
q.pop();
tag[now]=u;
f[1][now]=u;
}
else{
q.push(tag[now]);
tag[now]=0;
}
}
}
int main(){
read(n),read(m1),read(m2);
for(int i=1;i<=m1;i++){
read(g[0][i].a),read(g[0][i].b);
b[++cnt]=g[0][i].a,b[++cnt]=g[0][i].b;
}
if(n>=m1+m2){
printf("%d\n",m1+m2);
return 0;
}
sort(b+1,b+cnt+1);
int len=unique(b+1,b+cnt+1)-b-1;
maxt1=0;
for(int i=1;i<=m1;i++){
g[0][i].a=lower_bound(b+1,b+len+1,g[0][i].a)-b;
g[0][i].b=lower_bound(b+1,b+len+1,g[0][i].b)-b;
maxt1=max(maxt1,g[0][i].b);
}
cnt=0;
for(int i=1;i<=m2;i++){
read(g[1][i].a),read(g[1][i].b);
b[++cnt]=g[1][i].a,b[++cnt]=g[1][i].b;
}
sort(b+1,b+cnt+1);
len=unique(b+1,b+cnt+1)-b-1;
maxt2=0;
for(int i=1;i<=m2;i++){
g[1][i].a=lower_bound(b+1,b+len+1,g[1][i].a)-b;
g[1][i].b=lower_bound(b+1,b+len+1,g[1][i].b)-b;
maxt2=max(maxt2,g[1][i].b);
}
sort(g[1]+1,g[1]+m2);
sort(g[0]+1,g[0]+m1);
solve();
for(int i=1;i<=m1;i++){
pre[0][f[0][i]]++;
}
for(int i=1;i<=m2;i++){
pre[1][f[1][i]]++;
}
for(int i=1;i<=maxt1;i++){
pre[0][i]+=pre[0][i-1];
}
for(int i=1;i<=maxt2;i++){
pre[1][i]+=pre[1][i-1];
}
int ans=0;
for(int i=0;i<=n;i++){
int red=n-i;
ans=max(ans,pre[0][i]+pre[1][red]);
}
printf("%d\n",ans);
return 0;
}
T2.括号序列
怎么又是括号序列,考场上直接跳了。好像大家都做出来了,蒟蒻太菜了/kk。
正解是区间DP,可以不动脑子的设计状态:令 \(f(i,j),g(i,j),h(i,j),p(i,j),q(i,j)\) 分别表示区间 \([i,j]\) 中替换 \(?\) 得到 \((A'), A,SA,AS,S\) 的方案数,\((A')\) 表示最外层为一对匹配括号的超级括号序列。
初始化:所有长度为 \(1\) 的区间只有是 \(*\) 或者 \(?\) 时,\(q\) 为 $ 1$ 。其余均为 $0 $。
考虑如何转移。
首先是 \(q\) 。\(q(l,r)=1\) 当且仅当 \(s[l..r]\) 全是 \(?\) 或者 \(*\) 。
接下来考虑 \(h,p\) 。 \(h,p\) 转移注意一下转移边界。
接着是 \(f\) 。\(f\) 最外层有一对配对括号,所以 \(f\) 只能为 \((A),(SA),(AS),(),(S)\) 这五种情况。
最后是 \(g\) 。\(AB\) 和 \(ASB\) 其实都可以看做后者(对于前者让 \(|S|=0\) 就可以了)。那么 \(g\) 所表示的最终形式一定形如 (...)***(...)***(...)
。我们转移时枚举第一个右括号的位置,因为我们已经计算过 \(f,q,h\) 了,所以转移就是把这三个拼起来。最后再把 \(f(l,r)\) 加上。
\(h(l,r)=\sum\limits_{i=l}^{r-1}q(l,i)*g(i+1,r)\)
\(p(l,r)=\sum\limits_{i=l+1}^{r-1}g(l,i)*q(i+1,r)\)
\(f(l,r)=g(l+1,r-1)+h(l+1,r-1)+p(l+1,r-1)+q(l+1,r-1)+[r-l+1=2]*[s[l..r]=()]\)
\(g(l,r)=f(l,r)+\sum\limits_{i=l+1}^{r-1}f(l,i)*(h(i+1,r)+g(i+1,r))\)
时间复杂度:\(\mathcal{O}(n^3)\)
Code:
int n,k;
int f[DMAX][DMAX],g[DMAX][DMAX];
int h[DMAX][DMAX],p[DMAX][DMAX];
int q[DMAX][DMAX];
char s[DMAX];
// f:(A') g:A h:SA p:AS q:S
int main(){
read(n),read(k);
scanf("%s",s+1);
for(int i=1;i<=n;i++){
if(s[i]=='*' || s[i]=='?'){
q[i][i]=1;
}
}
for(int len=2;len<=n;len++){
for(int l=1;l<=n-len+1;l++){
int r=l+len-1;
if(r-l+1>k){
q[l][r]=0;
}
else{
bool flag=0;
for(int i=l;i<=r;i++){
if(s[i]!='*' && s[i]!='?'){
q[l][r]=0;
flag=1;
break;
}
}
if(!flag){
q[l][r]=1;
}
}
if(r-l+1==2){
if((s[l]=='(' && s[r]==')') || (s[l]=='(' && s[r]=='?') || (s[l]=='?' && s[r]==')') || (s[l]=='?' && s[r]=='?')){
f[l][r]=(0ll+f[l][r]+1)%MOD;
}
}
if((s[l]=='(' && s[r]==')') || (s[l]=='(' && s[r]=='?') || (s[l]=='?' && s[r]==')') || (s[l]=='?' && s[r]=='?')){
if(q[l+1][r-1]){
f[l][r]=(f[l][r]+1)%MOD;
}
f[l][r]=(0ll+f[l][r]+g[l+1][r-1])%MOD;
f[l][r]=(0ll+f[l][r]+h[l+1][r-1])%MOD;
f[l][r]=(0ll+f[l][r]+p[l+1][r-1])%MOD;
}
for(int i=l+1;i<=r-1;i++){
int now=(0ll+h[i+1][r]+g[i+1][r])%MOD;
g[l][r]=(0ll+g[l][r]+1ll*f[l][i]*now%MOD)%MOD;
}
for(int i=l;i<=r-1;i++){
h[l][r]=(0ll+h[l][r]+1ll*q[l][i]*g[i+1][r]%MOD)%MOD;
}
for(int i=l+1;i<=r-1;i++){
p[l][r]=(0ll+p[l][r]+1ll*g[l][i]*q[i+1][r]%MOD)%MOD;
}
g[l][r]=(0ll+g[l][r]+f[l][r])%MOD;
}
}
printf("%d\n",g[1][n]);
return 0;
}
T3.回文
考场上只有暴力,没看出来结论,好像大眼看样例就能看出来,我眼太小(
直接给结论:对于前 \(n\) 个删除的数,这 \(n\) 个数组成的排列 \(p\) 的任意前缀,都是后 \(n\) 个数的一个区间。(充要条件)
结论的话手玩一下应该就能发现。不过既然是结论还是考虑证明一下吧。前 \(n\) 次选出来的数形成的序列也就是 \(b\) 的前 \(n\) 个数,而这样它的任意前缀即为 \(b\) 的任意前缀,同时由于 \(b\) 是回文,所以也是任意后缀。那这样一定是连续被选择出来添加到末尾的,即一定是一段区间。
那么我们现在只需考虑第一个数取哪个,分类讨论即可。选数的时候贪心就可以了,优先选择从左边取,选的同时不断地拓展区间。选完 \(n\) 个数,剩下 \(n\) 个稍微操作一下就行了。
时间复杂度:\(\mathcal{O}(Tn)\)
Code:
int T;
int n;
int a[DMAX<<1];
int pos[DMAX];
int link[DMAX<<1];
string s;
int st[DMAX],top;
bool tak[DMAX<<1];
bool hve=0;
void solve1(){
int l,r;
l=r=link[1];
int dl=2,dr=2*n;
mem(tak,0);
tak[a[1]]=1;
int hve=1;
string t="L";
top=0;
st[++top]=a[1];
bool find=1;
while(1){
if(hve==n){
break;
}
if(l-link[dl]==1 && !tak[a[dl]]){
t+='L';
st[++top]=a[dl];
tak[a[dl]]=1;
dl++,l--,hve++;
continue;
}
if(link[dl]-r==1 && !tak[a[dl]]){
t+='L';
st[++top]=a[dl];
tak[a[dl]]=1;
dl++,r++,hve++;
continue;
}
if(link[dr]-r==1 && !tak[a[dr]]){
t+='R';
st[++top]=a[dr];
tak[a[dr]]=1;
dr--,r++,hve++;
continue;
}
if(l-link[dr]==1 && !tak[a[dr]]){
t+='R';
st[++top]=a[dr];
tak[a[dr]]=1;
dr--,l--,hve++;
continue;
}
find=0;
break;
}
if(!find){
return ;
}
while(dl<=dr){
if(a[dl]==st[top]){
t+='L';
dl++,top--;
continue;
}
if(a[dr]==st[top]){
t+='R';
dr--,top--;
continue;
}
find=0;
break;
}
if(!find){
return ;
}
s=t;
}
void solve2(){
int l,r;
l=r=link[2*n];
int dl=1,dr=2*n-1;
int hve=1;
string t="R";
top=0;
st[++top]=a[2*n];
mem(tak,0);
tak[a[2*n]]=1;
bool find=1;
while(1){
if(hve==n){
break;
}
if(l-link[dl]==1 && !tak[a[dl]]){
t+='L';
st[++top]=a[dl];
tak[a[dl]]=1;
dl++,l--,hve++;
continue;
}
if(link[dl]-r==1 && !tak[a[dl]]){
t+='L';
st[++top]=a[dl];
tak[a[dl]]=1;
dl++,r++,hve++;
continue;
}
if(link[dr]-r==1 && !tak[a[dr]]){
t+='R';
st[++top]=a[dr];
tak[a[dr]]=1;
dr--,r++,hve++;
continue;
}
if(l-link[dr]==1 && !tak[a[dr]]){
t+='R';
st[++top]=a[dr];
tak[a[dr]]=1;
dr--,l--,hve++;
continue;
}
find=0;
break;
}
if(!find){
return ;
}
while(dl<=dr){
if(a[dl]==st[top]){
t+='L';
dl++,top--;
continue;
}
if(a[dr]==st[top]){
t+='R';
dr--,top--;
continue;
}
find=0;
break;
}
if(!find){
return ;
}
if(s=="?"){
s=t;
}
}
int main(){
freopen("palin.in","r",stdin);
freopen("palin.out","w",stdout);
read(T);
while(T--){
read(n);
mem(link,0);
mem(pos,0);
for(int i=1;i<=2*n;i++){
read(a[i]);
if(!pos[a[i]]){
pos[a[i]]=i;
}
else{
link[i]=pos[a[i]];
link[pos[a[i]]]=i;
}
}
hve=0;
s="?";
solve1();
if(s!="?"){
cout<<s<<endl;
continue;
}
solve2();
if(s=="?"){
printf("-1\n");
}
else{
cout<<s<<endl;
}
}
return 0;
}
T4.交通规划
考场上想到了 \(k=2\) 的最小割,也做过狼抓兔子。但是我觉得CSP不会考网络流,就没写。血亏!(现在还只会暴力和 \(k=2\) 的做法)
对于 \(k=2\) 的情况,如果两个附加点颜色相同,那么答案为 \(0\) 。如果不同,那就是经典的平面图最小割问题。这个可以转化为对偶图最短路。建图可以如下建图:
(绿叉表示白色点,棕叉表示黑色点),我们求的就是红色勾到蓝色勾的最短路。
时间复杂度:\(\mathcal{O}(Tnm\log nm)\)
Code:
struct edge{
int to,nxt;
ll len;
int from;
};
edge g[DMAX<<1];
int head[DMAX],cnt=0;
void addedge(int u,int v,ll w){
g[++cnt].to=v;
g[cnt].from=u;
g[cnt].nxt=head[u];
g[cnt].len=w;
head[u]=cnt;
}
int n,m,T,q;
int lef[505][505],up[505][505];
struct extra{
int x,y;
int col;
int lab;
int val;
int sx,sy;
int lx,ly;
};
extra a[55];
int col[5][5];
ll ans;
void DFS(int x,int y,ll now){
if(now>ans){
return ;
}
if(x==n+1 && y==1){
ll cost=now;
for(int i=1;i<=q;i++){
if(a[i].col!=col[a[i].lx][a[i].ly]){
cost+=a[i].val;
}
}
ans=min(ans,cost);
return ;
}
col[x][y]=0;
ll res=now;
if(col[x-1][y]==1){
res+=up[x][y];
}
if(col[x][y-1]==1){
res+=lef[x][y];
}
if(y+1>m){
DFS(x+1,1,res);
}
else{
DFS(x,y+1,res);
}
col[x][y]=1;
res=now;
if(col[x-1][y]==0){
res+=up[x][y];
}
if(col[x][y-1]==0){
res+=lef[x][y];
}
if(y+1>m){
DFS(x+1,1,res);
}
else{
DFS(x,y+1,res);
}
col[x][y]=0;
}
int Position(int x,int y){
return (x-1)*(m+1)+y;
}
ll dis[DMAX];
bool vst[DMAX];
int linx[DMAX],liny[DMAX];
int redf[DMAX];
queue<int> dq[2];
void Dij(){
for(int i=0;i<=260100;i++){
dis[i]=1e18;
vst[i]=0;
}
priority_queue<pair<ll,int> > qd;
int kpo=0;
int st=a[1].lab,ed=a[2].lab;
if(st>ed){
swap(st,ed);
}
while(!dq[0].empty()){
dq[0].pop();
}
st++;
while(1){
if(dis[redf[st]]==0){
break;
}
dq[kpo].push(redf[st]);
if(kpo==0){
qd.push(mp(0,redf[st]));
dis[redf[st]]=0;
}
if(st==ed){
kpo^=1;
}
st++;
if(st==2*n+2*m+1){
st=1;
}
}
while(!qd.empty()){
int u=qd.top().second;
qd.pop();
if(vst[u]){
continue;
}
vst[u]=1;
for(int i=head[u];i;i=g[i].nxt){
int v=g[i].to;
if(dis[v]>dis[u]+g[i].len){
dis[v]=dis[u]+g[i].len;
if(!vst[v]){
qd.push(mp(-dis[v],v));
}
}
}
}
return ;
}
int main(){
read(n),read(m),read(T);
for(int i=1;i<=n-1;i++){
for(int j=1;j<=m;j++){
read(up[i+1][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<m;j++){
read(lef[i][j+1]);
}
}
int pos;
while(T--){
read(q);
ans=1e18;
for(pos=1;pos<=2*m+2*n;pos++){
int i=0;
if(pos>=1 && pos<=m){
a[i].x=0,a[i].y=pos;
a[i].lx=1,a[i].ly=pos;
a[i].sx=1,a[i].sy=pos;
linx[pos]=a[i].sx,liny[pos]=a[i].sy;
redf[pos]=Position(linx[pos],liny[pos]);
}
if(pos>=m+1 && pos<=n+m){
a[i].x=pos-m,a[i].y=m+1;
a[i].lx=pos-m,a[i].ly=m;
a[i].sx=pos-m,a[i].sy=m+1;
linx[pos]=a[i].sx,liny[pos]=a[i].sy;
redf[pos]=Position(linx[pos],liny[pos]);
}
if(pos>=m+n+1 && pos<=2*m+n){
a[i].x=n+1,a[i].y=m-(pos-m-n)+1;
a[i].lx=n,a[i].ly=a[i].y;
a[i].sx=n+1,a[i].sy=a[i].y;
linx[pos]=a[i].sx,liny[pos]=a[i].sy+1;
redf[pos]=Position(linx[pos],liny[pos]);
}
if(pos>=2*m+n+1 && pos<=2*m+2*n){
a[i].x=n-(pos-2*m-n)+1,a[i].y=0;
a[i].lx=a[i].x,a[i].ly=1;
a[i].sx=a[i].lx,a[i].sy=a[i].y;
linx[pos]=a[i].sx+1,liny[pos]=a[i].sy+1;
redf[pos]=Position(linx[pos],liny[pos]);
}
}
for(int i=1;i<=q;i++){
read(a[i].val),read(pos);
read(a[i].col);
a[i].lab=pos;
if(pos>=1 && pos<=m){
a[i].x=0,a[i].y=pos;
a[i].lx=1,a[i].ly=pos;
a[i].sx=1,a[i].sy=pos;
}
if(pos>=m+1 && pos<=n+m){
a[i].x=pos-m,a[i].y=m+1;
a[i].lx=pos-m,a[i].ly=m;
a[i].sx=pos-m,a[i].sy=m+1;
}
if(pos>=m+n+1 && pos<=2*m+n){
a[i].x=n+1,a[i].y=m-(pos-m-n)+1;
a[i].lx=n,a[i].ly=a[i].y;
a[i].sx=n+1,a[i].sy=a[i].y;
}
if(pos>=2*m+n+1 && pos<=2*m+2*n){
a[i].x=n-(pos-2*m-n)+1,a[i].y=0;
a[i].lx=a[i].x,a[i].ly=1;
a[i].sx=a[i].lx,a[i].sy=a[i].y;
}
}
if(n<=5 && m<=5){
for(int i=0;i<=n+1;i++){
for(int j=0;j<=m+1;j++){
col[i][j]=-1;
}
}
DFS(1,1,0);
printf("%lld\n",ans);
continue;
}
mem(head,0);
cnt=0;
if(q==1){
printf("0\n");
continue;
}
if(q<=2){
if(a[1].col==a[2].col){
printf("0\n");
continue;
}
for(int i=1;i<=n-1;i++){
for(int j=1;j<=m;j++){
addedge(Position(i+1,j),Position(i+1,j+1),up[i+1][j]);
addedge(Position(i+1,j+1),Position(i+1,j),up[i+1][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<m;j++){
addedge(Position(i,j+1),Position(i+1,j+1),lef[i][j+1]);
addedge(Position(i+1,j+1),Position(i,j+1),lef[i][j+1]);
}
}
for(int i=1;i<=q;i++){
int x=a[i].sx,y=a[i].sy;
if(x==1 && y!=m+1 && y!=0){
addedge(Position(x,y),Position(x,y+1),a[i].val);
addedge(Position(x,y+1),Position(x,y),a[i].val);
continue;
}
if(y==m+1){
addedge(Position(x,y),Position(x+1,y),a[i].val);
addedge(Position(x+1,y),Position(x,y),a[i].val);
continue;
}
if(x==n+1){
addedge(Position(x,y),Position(x,y+1),a[i].val);
addedge(Position(x,y+1),Position(x,y),a[i].val);
continue;
}
if(y==0){
addedge(Position(x,y+1),Position(x+1,y+1),a[i].val);
addedge(Position(x+1,y+1),Position(x,y+1),a[i].val);
continue;
}
}
Dij();
ll red=INF;
while(!dq[1].empty()){
int u=dq[1].front();
dq[1].pop();
red=min(red,dis[u]);
}
ans=min(ans,red);
printf("%lld\n",ans);
}
}
return 0;
}
总结
好像我们省CSP有分就能进NOIP,所以只要不爆零就行。于是就随便考。
感觉今年比去年难度更大?NOIP是不是难度更大啊?退役了。
考场上策略还有点问题,模拟赛要认真打了。