暑假集训CSP提高模拟20
暑假集训CSP提高模拟20
组题人: @KafuuChinocpp
\(T1\) P191. Kanon \(0pts\)
-
将 \([a,a+1]\) 看做线段,雪球初始在端点上。
-
因为一个雪球不会被左边的雪球的左边的部分和右边的雪球的右边的部分贡献到,所以对这个雪球产生的贡献的雪块一定是一段连续的区间。
-
记 \(ld_{i},rd_{i}\) 分别表示雪球在第 \(i\) 天及之前向左,向右所能延伸的最长距离。
-
顺序处理相邻两个雪球间的雪块,二分出一个最小的 \(t\) 使得两个雪球所能扩展的最大距离有交叉,然后按时间顺序决定多少部分属于左边/右边。
点击查看代码
ll a[200010],ld[200010],rd[200010],weight[200010]; bool check(ll mid,ll len) { return ld[mid]+rd[mid]>len; } int main() { ll n,q,x,sum=0,l,r,mid,ans,i; cin>>n>>q; for(i=1;i<=n;i++) { cin>>a[i]; } for(i=1;i<=q;i++) { cin>>x; sum+=x; ld[i]=max(ld[i-1],-sum); rd[i]=max(rd[i-1],sum); } weight[1]+=ld[q]; weight[n]+=rd[q]; for(i=1;i<=n-1;i++) { if(ld[q]+rd[q]<=a[i+1]-a[i]) { weight[i]+=rd[q]; weight[i+1]+=ld[q]; } else { l=1; r=q; ans=0; while(l<=r) { mid=(l+r)/2; if(check(mid,a[i+1]-a[i])==true) { ans=mid; r=mid-1; } else { l=mid+1; } } if(ld[ans-1]==ld[ans])//ans-1 时刻就加到右边了 { weight[i+1]+=ld[ans]; weight[i]+=a[i+1]-a[i]-ld[ans]; } else { weight[i]+=rd[ans]; weight[i+1]+=a[i+1]-a[i]-rd[ans]; } } } for(i=1;i<=n;i++) { cout<<weight[i]<<endl; } return 0; }
\(T2\) P154. Summer Pockets \(0pts\)
-
设
Y
的数量为 \(cnt\) ,最后放了 \(x-1\) 个横向栅栏和 \(y-1\) 个竖向栅栏,即形成了 \(xy\) 个连通块,此时有 \(xy=\frac{cnt}{2}\) 。 -
考虑枚举 \(x\) ,接着有 \(y=\frac{cnt}{2x}\) ,然后考虑
check
。只考虑横向划分时,每一个块里恰好要有 \(2y\) 个Y
。竖向划分同理。横向划分的边界一定会在空行徘徊(如果有空行的话)。中途进行是否能划分或最后二维前缀和进行 \(check\) 即可。 -
时间复杂度为 \(O(hw \times \tau(hw))\) ,因为 \(\tau(hw)\) 不会太大所以可以通过。
点击查看代码
const ll p=998244353; ll sum[2010][2010]; vector<ll>hang,lie; char c[2010][2010]; ll ask(ll x1,ll y1,ll x2,ll y2) { return sum[x2][y2]-sum[x2][y1-1]-sum[x1-1][y2]+sum[x1-1][y1-1]; } int main() { ll h,w,ans=0,cnt=0,num,tot,flag,h_cnt,l_cnt,i,j,x,y; cin>>h>>w; for(i=1;i<=h;i++) { for(j=1;j<=w;j++) { cin>>c[i][j]; cnt+=(c[i][j]=='Y'); sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+(c[i][j]=='Y'); } } if(cnt%2==1) { cout<<"0"<<endl; } else { cnt/=2; for(x=1;x<=min(h,cnt);x++) { if(cnt%x==0) { y=cnt/x; if(y<=w) { h_cnt=l_cnt=1; flag=0; hang.clear(); lie.clear(); hang.push_back(1); lie.push_back(1); for(i=j=1;i<=h;i=j) { tot=0; num=1; for(;j<=h&&tot<2*y;j++) { tot+=ask(j,1,j,w); } for(;j<=h&&ask(j,1,j,w)==0;j++) { num++; } hang.push_back(j); if(j<=h) { h_cnt=h_cnt*num%p; } } for(i=j=1;i<=w;i=j) { tot=0; num=1; for(;j<=w&&tot<2*x;j++) { tot+=ask(1,j,h,j); } for(;j<=w&&ask(1,j,h,j)==0;j++) { num++; } lie.push_back(j); if(j<=w) { l_cnt=l_cnt*num%p; } } for(i=0;i<hang.size()-1;i++) { for(j=0;j<lie.size()-1;j++) { flag|=(ask(hang[i],lie[j],hang[i+1]-1,lie[j+1]-1)!=2); } } ans=(ans+(flag^1)*h_cnt*l_cnt%p)%p; } } } cout<<ans<<endl; } return 0; }
\(T3\) P199. 空之境界 \(60pts\)
-
部分分
- \(60pts\)
-
区间 \(DP\) 。设 \(f_{l,r}\) 表示破坏 \([l,r]\) 单次需要输出的最小魔力值,状态转移方程为 \(f_{l,r}=\min\{\max(f_{l+1,r-1},cost_{l,r}), \max\limits_{k=l+1}^{r-1}(f_{l,k},f_{k+1,r}) \}\) 。时间复杂度为 \(O(n^{3})\) ,因只有偶区间才有用所以带 \(\frac{1}{4}\) 的常数。
点击查看代码
int cost[4010][4010],f[4010][4010]; int main() { int n,i,j,k,l,r,len; scanf("%d",&n); for(i=1;i<=n-1;i++) { for(j=i+1;j<=n;j+=2) { scanf("%d",&cost[i][j]); } } for(len=2;len<=n;len+=2) { for(l=1,r=l+len-1;r<=n;l++,r++) { f[l][r]=max(f[l+1][r-1],cost[l][r]); for(k=l+1;k<=r-1;k+=2) { f[l][r]=min(f[l][r],max(f[l][k],f[k+1][r])); } } } printf("%d\n",f[1][n]); return 0; }
-
状态转移方程本质上是个让最大值最小的过程,考虑二分答案,仍用区间 \(DP\) 的方式通过 \(0/1\) 进行转移,状态转移方程为 \(f_{l,r}=[f_{l+1,r-1}=1 \land cost_{l,r} \le mid] \lor [\exists k \in (l,r),f_{l,k}=1 \land f_{k+1,r}=1]\) 。拿
bitset
压一下,时间复杂度为 \(O(\frac{n^{3} \log n}{w})\) ,带 \(\frac{1}{4}\) 左右的常数。点击查看代码
int cost[4010][4010]; bitset<4010>f[4010]; bool check(int mid,int n) { for(int i=1;i<=n;i++) { f[i].reset(); f[i][i-1]=1; } for(int len=2;len<=n;len+=2) { for(int l=1,r=l+len-1;r<=n;l++,r++) { f[l][r]=(f[l+1][r-1]&(cost[l][r]<=mid)); for(int k=l+1;k<=r-1;k+=2) { f[l][r]=f[l][r]|(f[l][k]&f[k+1][r]); } } } return (f[1][n]==1); } int main() { int n,i,j,l=1,r,mid,ans=0; scanf("%d",&n); r=n*n/4; for(i=1;i<=n-1;i++) { for(j=i+1;j<=n;j+=2) { scanf("%d",&cost[i][j]); } } while(l<=r) { mid=(l+r)/2; if(check(mid,n)==true) { ans=mid; r=mid-1; } else { l=mid+1; } } printf("%d\n",ans); return 0; }
-
- \(60pts\)
-
正解
- 观察到 \(f_{l,r}\) 在一个比当前答案小的时候就更新成了 \(1\) ,后续在答案越来越大的时刻永远不会变成 \(0\) ,考虑利用这个状态的继承性来省去二分。
- 考虑选择优化转移过程,转移点至多有 \(n^{2}\) 个。假设我们当前把 \(f_{l,r}\) 设成了 \(1\) ,那么满足 \(f_{r+1,s}=1\) 的 \(s\) 就能转移到 \(f_{l,s}\) , \(f_{s',l-1}\) 同理。通过
bitset
的_Find_first()
和_Find_next(test)
或进行或运算来进行跳转即可。 - 直接枚举答案或二分答案,后者理论时间复杂度仍为 \(O(\frac{n^{3} \log n}{w})\) ,但能过。
- 前者比较难写,挂一下下发的官方题解和 QOJ 题解 。
具体的,我们从 \(1\) 到 \(\tfrac{n^2}{4}\) 枚举答案,假设当前枚举的答案为 \(i\) ,并且我们已经得到了答案为 \(i\) 时 \(f\) 的所有 \(0/1\) 取值,考虑现在枚举的答案为 \(i + 1\) ,此时我们考虑 \(f\) 中哪些位置会从 \(0\) 变为 \(1\) ,容易发现这些位置满足其暴力 dp 的答案为 \(i + 1\) ,因此这些位置一定是从满足 \(cost(L, R) = i + 1\) 的位置为起点转移过来的。我们考虑找到 \(cost(L, R) = i + 1\) 的位置,并把这个位置塞进队列中,每次我们取出队列中的一个位置 \((L, R)\) ,使用 bitset 找到当前位置能够更新到的所有位置,同样塞进队列中。
这样转移,每个位置 \((L, R)\) 只会被它的最优决策点转移一次,总复杂度为 \(O(\tfrac{n^3}{w})\) 。
点击查看代码
int cost[4010][4010]; bitset<4010>f[4010]; bool check(int mid,int n) { for(int i=1;i<=n;i++) { f[i].reset(); f[i][i-1]=1; } for(int l=n;l>=1;l--) { for(int r=l+1;r<=n;r+=2) { if(f[l][r]==0&&cost[l][r]<=mid&&f[l+1][r-1]==1)//判断是否有重复运算 { f[l][r]=1; f[l]|=f[r+1]; } } } return (f[1][n]==1); } int main() { int n,i,j,l=1,r,mid,ans=0; scanf("%d",&n); r=n*n/4; for(i=1;i<=n-1;i++) { for(j=i+1;j<=n;j+=2) { scanf("%d",&cost[i][j]); } } while(l<=r) { mid=(l+r)/2; if(check(mid,n)==true) { ans=mid; r=mid-1; } else { l=mid+1; } } printf("%d\n",ans); return 0; }
\(T4\) P128. 穗 \(80pts\)
-
部分分
- \(20 \%\) :暴力统计。
- 另外 \(20 \%\) :普通莫队或 \(CDQ\) 分治或扫描线套树状数组或主席树维护,同 luogu P1972 [SDOI2009] HH的项链 | AT_abc174_f [ABC174F] Range Set Query | SP3267 DQUERY - D-query 。
- 另外 \(40 \%\) :带修莫队或分块或 \(CDQ\) 分治或带修主席树维护,同 luogu P1903 [国家集训队] 数颜色 / 维护队列 | SP30906 ADAUNIQ - Ada and Unique Vegetable | UVA12345 Dynamic len(set(a[L:R])) | BZOJ2453 维护队列 。
点击查看代码
struct node { int pd,l,r,x; }d[200010]; struct ask { int l,r,tim,id; }q[200010]; struct change { int pos,col; }c[200010]; int a[200010],b[200010],cnt[200010],pos[200010],L[200010],R[200010],ans[200010],q_cnt=0,c_cnt=0,klen,ksum; bool q_cmp(ask a,ask b) { return (pos[a.l]==pos[b.l])?((pos[a.r]==pos[b.r])?(a.tim<b.tim):(a.r<b.r)):(a.l<b.l); } void add(int x,int &sum) { cnt[x]++; sum+=(cnt[x]==1); } void del(int x,int &sum) { cnt[x]--; sum-=(cnt[x]==0); } void init(int n) { klen=pow(n,2.0/3); ksum=n/klen; for(int i=1;i<=ksum;i++) { L[i]=R[i-1]+1; R[i]=R[i-1]+klen; } if(R[ksum]<n) { ksum++; L[ksum]=R[ksum-1]+1; R[ksum]=n; } for(int i=1;i<=ksum;i++) { for(int j=L[i];j<=R[i];j++) { pos[j]=i; } } } int main() { int n,m,flag=1,l=1,r=0,tim=0,sum=0,i,j; cin>>n>>m; for(i=1;i<=n;i++) { cin>>a[i]; b[0]++; b[i]=a[i]; } for(i=1;i<=m;i++) { cin>>d[i].pd>>d[i].l>>d[i].r; if(d[i].pd==1) { cin>>d[i].x; b[0]++; b[b[0]]=d[i].x; flag&=(d[i].l==d[i].r); } } sort(b+1,b+1+b[0]); b[0]=unique(b+1,b+1+b[0])-b; for(i=1;i<=n;i++) { a[i]=lower_bound(b+1,b+1+b[0],a[i])-b; } for(i=1;i<=m;i++) { if(d[i].pd==1) { d[i].x=lower_bound(b+1,b+1+b[0],d[i].x)-b; c_cnt++; c[c_cnt].pos=d[i].l; c[c_cnt].col=d[i].x; } else { q_cnt++; q[q_cnt].l=d[i].l; q[q_cnt].r=d[i].r; q[q_cnt].id=q_cnt; q[q_cnt].tim=c_cnt; } } if(flag==0) { for(i=1;i<=m;i++) { if(d[i].pd==1) { for(j=d[i].l;j<=d[i].r;j++) { a[j]=d[i].x; } } else { sum=0; for(j=d[i].l;j<=d[i].r;j++) { cnt[a[j]]++; sum+=(cnt[a[j]]==1); } for(j=d[i].l;j<=d[i].r;j++) { cnt[a[j]]--; } cout<<sum<<endl; } } } else { init(n); sort(q+1,q+1+q_cnt,q_cmp); for(i=1;i<=m;i++) { while(l>q[i].l) { l--; add(a[l],sum); } while(r<q[i].r) { r++; add(a[r],sum); } while(l<q[i].l) { del(a[l],sum); l++; } while(r>q[i].r) { del(a[r],sum); r--; } while(tim<q[i].tim) { tim++; if(l<=c[tim].pos&&c[tim].pos<=r) { del(a[c[tim].pos],sum); add(c[tim].col,sum); } swap(a[c[tim].pos],c[tim].col); } while(tim>q[i].tim) { if(l<=c[tim].pos&&c[tim].pos<=r) { del(a[c[tim].pos],sum); add(c[tim].col,sum); } swap(a[c[tim].pos],c[tim].col); tim--; } ans[q[i].id]=sum; } for(i=1;i<=q_cnt;i++) { cout<<ans[i]<<endl; } } return 0; }
-
正解
- 设 \(pre_{i}\) 表示 \(i\) 左侧第一个与 \(a_{i}\) 同色点的位置, \(nxt_{i}\) 表示 \(i\) 右侧第一个与 \(a_{i}\) 同色点的位置
- 询问等价于查询 \(\sum\limits_{i=l}^{r}[pre_{i} \le l-1]\) ,考虑二维数点。
- 单点修改时只有这个点和以这个点作为 \(pre\) 的点(即这个点的 \(nxt\) )的 \(pre\) 会发生变化,即变化的点是 \(O(1)\) 级别的。
- 由于对一个长度为 \(n\) 的序列进行 \(m\) 次区间推平操作, \(\{ pre \}\) 的改变次数是 \(O(n+m)\) 级别的,考虑珂朵莉树维护相同颜色的一段区间。
- 假设我们需要把 \([L,R]\) 全都推平成 \(C\) ,原 \([L,R]\) 分成了 \([l_{1},r_{1}],[l_{2},r_{2}], \dots ,[l_{k},r_{k}]\) 这 \(k\) 个部分,不难发现只有 \(\begin{cases} nxt_{l_{1}},nxt_{l_{2}}, \dots ,nxt_{l_{k}} \\ l_{2} \dots ,l_{k} \\ L,nxt_{L} \end{cases}\) 三部分的 \(pre\) 会发生变化,顺序处理这些变化(因为 \(l_{1}=L\) 所以最后的 \(L,nxt_{L}\) 要特殊处理)。
- 将相同颜色的一段区间视作一个点, \(\{ pre \}\) 的修改可以视作单点修改,套个 \(CDQ\) 分治维护带修二维数点即可。
- 为方便查找 \(pre\) ,同一颜色的点开个
set
存储位置,和珂朵莉树的set
一起进行加入、删除操作。
点击查看代码
struct node { int t,x,y,pd,id; bool operator < (const node &another) const { return (x==another.x)?((y==another.y)?(id<another.id):(y<another.y)):(x<another.x); } }q[1500010]; int a[100010],pre[100010],last[100010],ans[100010],cnt=0,q_cnt=0,tot=0; map<int,int>b;//选择合适的离散化方式 void add(int t,int x,int y,int pd,int id) { cnt++; q[cnt].t=t; q[cnt].x=x; q[cnt].y=y+1;//特殊处理树状数组里插 0 q[cnt].pd=pd; q[cnt].id=id; } struct BIT { int c[100010]; int lowbit(int x) { return (x&(-x)); } void add(int n,int x,int val) { for(int i=x;i<=n;i+=lowbit(i)) { c[i]+=val; } } int getsum(int x) { int ans=0; for(int i=x;i>=1;i-=lowbit(i)) { ans+=c[i]; } return ans; } }B; struct ODT { struct node { int l,r; mutable int col; bool operator < (const node &another) const { return l<another.l; } }; set<node>s,color[200010]; vector<int>tmp; void init(int n,int a[]) { for(int i=1;i<=n;i++) { insert(i,i,a[i]); } } set<node>::iterator insert(int l,int r,int col) { color[col].insert((node){l,r,col}); return s.insert((node){l,r,col}).first; } void erase(int l,int r,int col) { color[col].erase((node){l,r,col}); s.erase((node){l,r,col}); } set<node>::iterator split(int pos) { set<node>::iterator it=s.lower_bound((node){pos,0,0}); if(it!=s.end()&&it->l==pos) { return it; } it--; if(it->r<pos) { return s.end(); } int l=it->l,r=it->r,col=it->col; erase(l,r,col); insert(l,pos-1,col); return insert(pos,r,col); } int find_pre(int pos) { set<node>::iterator it=--s.upper_bound((node){pos,0,0}); if(it->l<pos)//太大了 { return pos-1; } set<node>::iterator itc=color[it->col].lower_bound((node){pos,0,0}); if(itc!=color[it->col].begin()) { itc--; return itc->r; } return 0; } void assign(int l,int r,int col,int t) { set<node>::iterator itr=split(r+1),itl=split(l); tmp.clear(); for(set<node>::iterator it=itl;it!=itr;it++) { if(it!=itl) { tmp.push_back(it->l); } set<node>::iterator itc=color[it->col].upper_bound(*it);//找后继 if(itc!=color[it->col].end()) { tmp.push_back(itc->l);//这里的乱序无所谓 } color[it->col].erase(*it); } s.erase(itl,itr); insert(l,r,col); tmp.push_back(l);//最后再处理 set<node>::iterator itc=color[col].upper_bound((node){l,r,col}); if(itc!=color[col].end()) { tmp.push_back(itc->l);//最后再处理 } for(int i=0;i<tmp.size();i++) { add(t,tmp[i],pre[tmp[i]],-1,0); pre[tmp[i]]=find_pre(tmp[i]); add(t,tmp[i],pre[tmp[i]],1,0); } } }O; void cdq(int l,int r,int k) { if(l==r) { return; } int mid=(l+r)/2,x,y; cdq(l,mid,k); cdq(mid+1,r,k); for(x=l,y=mid+1;y<=r;y++) { for(;x<=mid&&q[x]<q[y];x++) { B.add(k,q[x].y,(q[x].id==0)*q[x].pd); } ans[q[y].id]+=q[y].pd*B.getsum(q[y].y); } x--; for(int i=l;i<=x;i++) { B.add(k,q[i].y,-(q[i].id==0)*q[i].pd); } inplace_merge(q+l,q+mid+1,q+r+1); } int main() { int n,m,pd,l,r,x,i; cin>>n>>m; for(i=1;i<=n;i++) { cin>>a[i]; if(b.find(a[i])==b.end()) { tot++; b[a[i]]=tot; } a[i]=b[a[i]]; pre[i]=last[a[i]]; last[a[i]]=i; add(0,i,pre[i],1,0); } O.init(n,a); for(i=1;i<=m;i++) { cin>>pd>>l>>r; if(pd==1) { cin>>x; if(b.find(x)==b.end()) { tot++; b[x]=tot; } x=b[x]; O.assign(l,r,x,i); } else { q_cnt++; add(i,r,l-1,1,q_cnt); add(i,l-1,l-1,-1,q_cnt); } } cdq(1,cnt,n+1); for(i=1;i<=q_cnt;i++) { cout<<ans[i]<<endl; } return 0; }
总结
- \(T1,T2\) 赛时没读懂什么意思,手摸样例也失败了,赛后发现题读假了。
- \(T3\) 貌似是自己第一次独立做稍微困难点的区间 \(DP\) ,感觉对区间 \(DP\) 的转移有了更深刻的认识;想出二分后感觉为一个 \(0.34\) 的常数去写二分加
bitset
优化还照样过不去就没写,然后就不会去掉 \(\log\) 了;尝试想 \(Kruskal\) 重构树发现不会建图。
后记
-
组题人以后组题的时候能不能别整些题意不清晰的题啊。
- \(T2\) 没解释 放完整行或整列 是指栅栏必须一整行或整列得放,而不是必须全放栅栏。
- \(T3\) 没说单次输出的魔力值是固定的,在手摸最小化总魔力值、最小化最大魔力值、最大化最小魔力值后发现最小化最大魔力值是对的。
- \(T4\) 一开始没有 \(m\) 的数据范围。
-
没有特殊奖励。
-
但有特殊惩罚。
-
众多提醒(自下而上)。
-
@H_Kaguya 瑞平组题人 某毒瘤出题人(密码看摘要) 。
-
题目背景夹带私货。
本文来自博客园,作者:hzoi_Shadow,原文链接:https://www.cnblogs.com/The-Shadow-Dragon/p/18358880,未经允许严禁转载。
版权声明:本作品采用 「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0) 进行许可。