暑假集训CSP提高模拟16
暑假集训CSP提高模拟16
组题人: @Muel_imj
\(T1\) P216. 九次九日九重色 \(100pts\)
- 部分分
- \(30pts\) :设 \(f_{i,j}\) 表示当前处理到 \(P\) 的第 \(i\) 位,\(Q\) 的第 \(j\) 位时最大的 \(k\) ,状态转移方程为 \(f_{i,j}=\begin{cases} \max(f_{i,j-1},f_{i-1,j}) & p_{i} \nmid q_{j} \\ \max(f_{i,j-1},f_{i-1,j},f_{i-1,j-1}+1) & p_{i}|q_{j} \end{cases}\) 。最终,有 \(f_{n,n}\) 即为所求。
- 正解
-
尝试转化到最长公共子序列上去。
-
发现能对答案产生贡献的只有 \(p_{i}|q_{j}\) 的关键点,对关键点的转移只有 \(f_{i-1,j-1}\) 有用。
-
记录关键点的位置,树状数组维护前缀 \(\max\) 即可,做法同 luogu P4303 [AHOI2006] 基因匹配 。
点击查看代码
int p[200010],q[200010],f[200010]; vector<int>pos[200010]; struct BIT { int c[200010]; 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]=max(c[i],val); } } int getsum(int x) { int ans=0; for(int i=x;i>=1;i-=lowbit(i)) { ans=max(ans,c[i]); } return ans; } }T; int main() { int n,ans=0,i,j; cin>>n; for(i=1;i<=n;i++) { cin>>p[i]; for(j=1;j*p[i]<=n;j++) { pos[j*p[i]].push_back(i); } } for(i=1;i<=n;i++) { cin>>q[i]; } for(i=1;i<=n;i++) { if(pos[q[i]].size()>=1) { for(j=pos[q[i]].size()-1;j>=0;j--)//防止有后效性 { f[pos[q[i]][j]]=T.getsum(pos[q[i]][j]-1)+1;//因为转移过程是单调不降的,所以不用取 max T.add(n,pos[q[i]][j],f[pos[q[i]][j]]); } } } for(i=1;i<=n;i++) { ans=max(ans,f[i]); } cout<<ans<<endl; return 0; }
-
官方题解写的也很抽象,挂一下吧。
-
\(T2\) P217. 天色天歌天籁音 \(80pts\)
-
部分分
-
\(10pts\) :输出 \(-1\) 。
-
-
正解
-
懒得复制一遍了,直接粘自己当时写的 题解链接 了。
点击查看代码
int a[200010],b[200010],pos[200010],L[200010],R[200010],ans[200010],num[200010],cnt[200010],klen,ksum; struct ask { int l,r,id; }q[200010]; bool q_cmp(ask a,ask b) { return (pos[a.l]==pos[b.l])?((pos[a.l]%2==1)?(a.r<b.r):(a.r>b.r)):(a.l<b.l); } void init(int n,int m) { klen=n/sqrt(m)+1; 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; } } } void add(int x,int &sum) { num[cnt[a[x]]]--; cnt[a[x]]++; num[cnt[a[x]]]++; sum=max(sum,cnt[a[x]]); } void del(int x,int &sum) { num[cnt[a[x]]]--; sum-=(sum==cnt[a[x]]&&num[cnt[a[x]]]==0); cnt[a[x]]--; num[cnt[a[x]]]++; } int main() { int n,m,l=1,r=0,sum=0,i; cin>>n>>m; for(i=1;i<=n;i++) { cin>>a[i]; b[i]=a[i]; } sort(b+1,b+1+n); b[0]=unique(b+1,b+1+n)-(b+1); for(i=1;i<=n;i++) { a[i]=lower_bound(b+1,b+1+b[0],a[i])-b; } init(n,m); for(i=1;i<=m;i++) { cin>>q[i].l>>q[i].r; q[i].id=i; } sort(q+1,q+1+m,q_cmp); for(i=1;i<=m;i++) { while(l>q[i].l) { l--; add(l,sum); } while(r<q[i].r) { r++; add(r,sum); } while(l<q[i].l) { del(l,sum); l++; } while(r>q[i].r) { del(r,sum); r--; } ans[q[i].id]=-sum; } for(i=1;i<=m;i++) { cout<<ans[i]<<endl; } return 0; }
-
\(T3\) P218. 春色春恋春熙风 \(20pts\)
- 原题: CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
- 字符重排后可以形成回文串当且仅当至多有一个字符的出现次数为奇数。
- 部分分
- \(20pts\) :\(O(n^{2}|\sum|)\) 预处理出任意两点间是否存在「春」,转成 \(DFS\) 序后查询时等价于查询一个矩阵的最大值,暴力枚举即可。
- 正解
-
考虑将奇偶状压一下,用异或来进行修改和查询。
-
设 \(sum_{x}\) 表示 \(1\) 到 \(x\) 的路径上的字符形成的状态,那么若 \(x \to y\) 的路径是「春」当且仅当 \(sum_{x} \bigoplus sum_{y}\) 至多有一位为 \(1\) ,即最终的结果的只有 \(\{ 000 \dots 0,100 \dots 0,010 \dots 0,001 \dots 0, \dots ,000 \dots 1\}\) 。
-
设 \(f_{x}\) 表示 \(1\) 到某个节点的路径上的字符形成状态为 \(x\) 时这个节点的最大深度。
-
树上启发式合并维护即可。
点击查看代码
struct node { int nxt,to; }e[1000010]; int head[1000010],c[1000010],ans[1000010],cnt=0; void add(int u,int v) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; head[u]=cnt; } int val(char x) { return x-'a'; } struct DSU_Tree { int fa[1000010],siz[1000010],dfn[1000010],out[1000010],pos[1000010],son[1000010],dep[1000010],sum[1000010],f[(1<<23)+10],tot=0; void init() { memset(f,-0x3f,sizeof(f)); } void add_rt1(int x,int rt) { ans[rt]=max(ans[rt],dep[x]+f[sum[x]]-2*dep[rt]);//此时两点的 lca 就是 rt for(int i=0;i<=21;i++) { ans[rt]=max(ans[rt],dep[x]+f[sum[x]^(1<<i)]-2*dep[rt]); } } void add_rt2(int x) { f[sum[x]]=max(f[sum[x]],dep[x]); } void add_rt(int x,int rt) { add_rt1(x,rt); add_rt2(x); } void del_rt(int x) { f[sum[x]]=-0x3f3f3f3f;//和 memset 赋的值不一样,但无伤大雅 } void add_tree(int x,int rt) { for(int i=dfn[x];i<=out[x];i++) { add_rt1(pos[i],rt); } for(int i=dfn[x];i<=out[x];i++) { add_rt2(pos[i]);//不能同时更新 } } void del_tree(int x) { for(int i=dfn[x];i<=out[x];i++) { del_rt(pos[i]); } } void ask(int x) { for(int i=head[x];i!=0;i=e[i].nxt) { ans[x]=max(ans[x],ans[e[i].to]); } } void dfs1(int x) { tot++; dfn[x]=tot; pos[tot]=x; siz[x]=1; dep[x]=dep[fa[x]]+1; sum[x]=(x!=1)*(sum[fa[x]]^(1<<c[x]));//特判根节点 for(int i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=fa[x]) { dfs1(e[i].to); siz[x]+=siz[e[i].to]; son[x]=(siz[e[i].to]>siz[son[x]])?e[i].to:son[x]; } } out[x]=tot; } void dfs2(int x,int flag) { for(int i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=son[x]&&e[i].to!=fa[x]) { dfs2(e[i].to,0); } } if(son[x]!=0) { dfs2(son[x],1); } for(int i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=son[x]&&e[i].to!=fa[x]) { add_tree(e[i].to,x); } } add_rt(x,x); ask(x);//统计子树内部的贡献 if(flag==0) { del_tree(x); } } }T; int main() { int n,i; char pd; cin>>n; for(i=2;i<=n;i++) { cin>>T.fa[i]>>pd; c[i]=val(pd); add(T.fa[i],i); } T.init(); T.dfs1(1); T.dfs2(1,1); for(i=1;i<=n;i++) { cout<<ans[i]<<" "; } return 0; }
-
\(T4\) P219. 雪色雪花雪余痕 \(10pts\)
-
部分分
- \(10pts\) :暴搜。
-
正解
- 限制条件移项后得到 \(a_{i}-a_{i-1} \le a_{i+1}-a_{i}\) ,有差分数组单调不降,即差分数组是一个单谷序列。
- 考虑从最小值所在位置及最小值数量入手,不妨考虑最左边的那个位置,然后就分割成了左右两个类似的子问题。对于子问题,原数组求和转化到差分数组求和是一个形如 \(\sum\limits_{i=1}^{n'}id_{i}=m'\) 的形式。
- 设 \(f_{i,j}\) 表示长度为 \(i\) 且和为 \(j\) 的方案数(其中 \(i\) 是 \(O(\sqrt{m})\) 级别的),暴力背包的时间复杂度为 \(O(n^{2})\) ,无法接受。类似分拆数根号分治后 \(O(n\sqrt{m})\) 的求法,将插入若干种数的操作转化为每个数都加 \(1\) 和在开头新加一个 \(1\) 这两种操作,状态转移方程为 \(f_{i,j}=f_{i,j-\frac{i(i-1)}{2}}+f_{i-1,j-i}\) ,边界为 \(f_{0,0}=1\) 。
- 先让最小值为 \(0\) ,此时其数量并不影响最终求和,故可以先对 \(\{ f \}\) 求前缀和,然后固定左边长度,枚举右边长度的过程中使得总长度 \(\le n\) 即可,空位(是右边的一段前缀)直接补 \(0\) 。
- 同时,将凸包抬升不改变凸包的性质,可以看做若干次将序列整体加 \(1\) 的完全背包合并过程(直接将方案数算到右边),统计答案时再进行合并。
点击查看代码
const ll p=1000000007; int f[450][100010],sum[450][100010],g[450][100010]; int main() { // #define Isaac #ifdef Isaac freopen("in.in","r",stdin); freopen("out.out","w",stdout); #endif int n,m,s,ans=0,i,j; cin>>n>>m; s=sqrt(2*m+2)+1; f[0][0]=sum[0][0]=1; for(i=0;i<=m;i+=n) { g[0][i]=1; } for(i=1;i<=s;i++) { for(j=0;j<=m;j++) { if(j>=i) { f[i][j]=(f[i][j]+f[i-1][j-i])%p; } if(j>=i*(i+1)/2) { f[i][j]=(f[i][j]+f[i][j-i*(i+1)/2])%p; } g[i][j]=sum[i][j]=(sum[i-1][j]+f[i][j])%p; if(j-n>=0) { g[i][j]=(g[i][j]+g[i][j-n])%p; } } } for(i=0;i<=min(n-1,s);i++) { for(j=0;j<=m;j++) { ans=(ans+1ll*f[i][j]*g[min(n-i-1,s)][m-j]%p)%p; } } cout<<ans<<endl; return 0; }
总结
- 赛时历程:莫名觉得 \(T1\) 不可做,所以先开 \(T2\) 。\(20 \min\) 码完怕拿首切没敢立刻交,大样例过了就没管。然后写 \(T3,T4\) 的部分分。给 \(T1\) 留了 \(1h+40 \min\) 的时间,结果想 \(40pts\) 的部分分就想了 \(30 \min\) ,然后在犹豫能不能和最长公共子序列一个做法,改完后发现过样例了就交上去了。
- \(T2\) 离散化的 \(n,m\) 写反了,挂了 \(20pts\) 。
后记
-
貌似是 GalGame 之 9-nine 专场。
-
\(T2\) 改后的题面还是很抽象。
本文来自博客园,作者:hzoi_Shadow,原文链接:https://www.cnblogs.com/The-Shadow-Dragon/p/18348962,未经允许严禁转载。
版权声明:本作品采用 「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0) 进行许可。