暑假集训CSP提高模拟19
暑假集训CSP提高模拟19
组题人: @Chen_jr
\(T1\) P173. 数字三角形 \(20pts\)
- 原题: CF1517C Fillomino 2
- 部分分
-
\(20pts\) :剪枝搜索。
点击查看代码
int p[510],c[510],ans[510][510],dx[5]={0,1,-1,0,0},dy[5]={0,0,0,-1,1}; void dfs(int pos,int x,int y,int num,int n) { if(pos==n+1) { for(int i=1;i<=n;i++) { for(int j=1;j<=i;j++) { cout<<ans[i][j]<<" "; } cout<<endl; } exit(0); } else { if(num==0) { dfs(pos+1,pos+1,pos+1,c[pos+1],n); } else { for(int i=1;i<=4;i++) { int nx=x+dx[i]; int ny=y+dy[i]; if(1<=nx&&nx<=n&&1<=ny&&ny<=x&&ans[nx][ny]==0) { ans[nx][ny]=p[pos]; dfs(pos,nx,ny,num-1,n); ans[nx][ny]=0; } } } } } int main() { int n,i,j; cin>>n; memset(ans,-1,sizeof(ans)); for(i=1;i<=n;i++) { cin>>p[i]; ans[i][i]=p[i]; c[i]=p[i]-1; } for(i=1;i<=n;i++) { for(j=1;j<=i-1;j++) { ans[i][j]=0; } } dfs(1,1,1,c[1],n); return 0; }
-
- 正解
-
通过打表,我们可以发现合法方案中同一个数向右延伸一定不优,向左延伸比向下延伸更优。
-
所以填数时优先向左延伸,否则向下延伸。
点击查看代码
int p[510],c[510],ans[510][510]; void dfs(int pos,int x,int y,int num,int n) { if(pos==n+1) { for(int i=1;i<=n;i++) { for(int j=1;j<=i;j++) { cout<<ans[i][j]<<" "; } cout<<endl; } exit(0); } else { if(num==0) { dfs(pos+1,pos+1,pos+1,c[pos+1],n); } else { if(y-1>=1&&ans[x][y-1]==0) { ans[x][y-1]=p[pos]; dfs(pos,x,y-1,num-1,n); } else { if(x+1<=n&&ans[x+1][y]==0) { ans[x+1][y]=p[pos]; dfs(pos,x+1,y,num-1,n); } } } } } int main() { int n,i; cin>>n; for(i=1;i<=n;i++) { cin>>p[i]; ans[i][i]=p[i]; c[i]=p[i]-1; } dfs(1,1,1,c[1],n); return 0; }
-
\(T2\) P160. 那一天她离我而去 \(0pts\)
- 弱化版: [ABC022C] Blue Bird
- 部分分
- \(76pts\) : \(Dijkstra\) 求解最小环。
-
枚举 \(1\) 的所有出边 \((1,x,w)\) ,钦定这条边是环上的边,删掉这条边后再求到这个点的最短路长度,加上被删的边的权值 \(w\) 与答案取 \(\min\) 即可。
-
若不限制起点,则需要枚举边 \((u,v,w) \in E\) ,删掉这条边后 \(v \to u\) 的最短路长度 \(+w\) 与答案取 \(\min\) 即可。时间复杂度为 \(O(m(n+m)\log n)\) 。
点击查看代码
struct node { int nxt,to,w; }e[80010]; int head[10010],vis[10010],dis[10010],brokeu,brokev,cnt=0; vector<pair<int,int> >broke; void add(int u,int v,int w) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; e[cnt].w=w; head[u]=cnt; } bool check(int u,int v) { return ((u==brokeu&&v==brokev)||(u==brokev&&v==brokeu)); } void dijkstra(int s) { memset(dis,0x3f,sizeof(dis)); memset(vis,0,sizeof(vis)); priority_queue<pair<int,int> >q; dis[s]=0; q.push(make_pair(-dis[s],-s)); while(q.empty()==0) { int x=-q.top().second; q.pop(); if(vis[x]==0) { vis[x]=1; for(int i=head[x];i!=0;i=e[i].nxt) { if(check(x,e[i].to)==0&&dis[e[i].to]>dis[x]+e[i].w) { dis[e[i].to]=dis[x]+e[i].w; q.push(make_pair(-dis[e[i].to],-e[i].to)); } } } } } int main() { int t,n,m,ans,u,v,w,i,j; cin>>t; for(j=1;j<=t;j++) { cnt=0; ans=0x7f7f7f7f; broke.clear(); memset(e,0,sizeof(e)); memset(head,0,sizeof(head)); cin>>n>>m; for(i=1;i<=m;i++) { cin>>u>>v>>w; add(u,v,w); add(v,u,w); if(u==1) { broke.push_back(make_pair(v,w)); } if(v==1) { broke.push_back(make_pair(u,w)); } } for(i=0;i<broke.size();i++) { brokeu=1; brokev=broke[i].first; dijkstra(1); if(dis[brokev]!=0x3f3f3f3f) { ans=min(ans,dis[brokev]+broke[i].second); } } cout<<(ans>=0x7f7f7f7f?-1:ans)<<endl; } return 0; }
-
- \(76pts\) : \(Dijkstra\) 求解最小环。
- 正解
-
考虑断掉最小环上的其他的边 \((u,v,w)\) 。
-
对与 \(1\) 相连的点进行二进制分组。
-
具体地,若与 \(1\) 相连的点 \(x\) ,当前一位是 \(1\) ,则保留边 \((1,x,w)\) ;否则删去这条边 \((1,x,w)\) ,从 \(x\) 向超级汇点 \(n+1\) 连一条权值为 \(w\) 的边。那么,到 \(n+1\) 的最短路长度与答案取 \(\min\) 即可。
- 本质上是一次删多条边,再多次统计。
- 最终答案要求断的那条边 \((u,v,w)\) 一定存在一个时刻被断掉了,所以是存在正确性的。
-
时间复杂度为 \(O((n+m) \log^{2} n+n \log n)\) 。
点击查看代码
int vis[10010],dis[10010],N; vector<pair<int,int> >broke,e[10010],ee[10010]; void add(int u,int v,int w,vector<pair<int,int> >e[]) { e[u].push_back(make_pair(v,w)); } void dijkstra(int s,vector<pair<int,int> >e[]) { memset(dis,0x3f,sizeof(dis)); memset(vis,0,sizeof(vis)); priority_queue<pair<int,int> >q; dis[s]=0; q.push(make_pair(-dis[s],-s)); while(q.empty()==0) { int x=-q.top().second; q.pop(); if(vis[x]==0) { vis[x]=1; for(int i=0;i<e[x].size();i++) { if(dis[e[x][i].first]>dis[x]+e[x][i].second) { dis[e[x][i].first]=dis[x]+e[x][i].second; q.push(make_pair(-dis[e[x][i].first],-e[x][i].first)); } } } } } int main() { int t,n,m,ans,u,v,w,i,j,k; cin>>t; for(k=1;k<=t;k++) { cin>>n>>m; N=log2(n)+1; ans=0x7f7f7f7f; broke.clear(); for(i=1;i<=n+1;i++) { e[i].clear(); ee[i].clear(); } for(i=1;i<=m;i++) { cin>>u>>v>>w; if(u==1||v==1) { if(u==1) { broke.push_back(make_pair(v,w)); } if(v==1) { broke.push_back(make_pair(u,w)); } } else { add(u,v,w,e); add(v,u,w,e); } } for(i=0;i<=N;i++) { for(j=1;j<=n;j++) { ee[j]=e[j]; } for(j=0;j<broke.size();j++) { if((broke[j].first>>i)&1) { add(1,broke[j].first,broke[j].second,ee); add(broke[j].first,1,broke[j].second,ee); } else { add(n+1,broke[j].first,broke[j].second,ee); add(broke[j].first,n+1,broke[j].second,ee); } } dijkstra(1,ee); if(dis[n+1]!=0x3f3f3f3f) { ans=min(ans,dis[n+1]); } } cout<<(ans>=0x7f7f7f7f?-1:ans)<<endl; } return 0; }
-
\(T3\) P175. 哪一天她能重回我身边 \(20pts\)
-
部分分
-
\(20pts\) :剪枝搜索。
点击查看代码
const ll p=998244353; ll x[100010],y[100010],cnt[200010],ans,sum; void dfs(ll pos,ll num,ll n) { if(pos==n+1) { if(num<ans) { ans=num; sum=1; } else { if(num==ans) { sum=(sum+1)%p; } } } else { cnt[x[pos]]++; if(cnt[x[pos]]==1) { dfs(pos+1,num,n); } cnt[x[pos]]--; cnt[y[pos]]++; if(cnt[y[pos]]==1) { dfs(pos+1,num+1,n); } cnt[y[pos]]--; } } int main() { ll t,n,i,j; cin>>t; for(j=1;j<=t;j++) { cin>>n; for(i=1;i<=n;i++) { cin>>x[i]>>y[i]; } ans=0x7f7f7f7f; sum=0; dfs(1,0,n); if(ans==0x7f7f7f7f) { cout<<"-1 -1"<<endl; } else { cout<<ans<<" "<<sum<<endl; } } return 0; }
-
-
正解
- 考虑建图,从每张牌的正面向反面连一条有向边,那么翻一张牌等价于把边反向。
- 具体实现时,我们令从正面向反面连一条权值为 \(1\) 的有向边,从反面向正面连一条权值为 \(0\) 的无向边,来表示到底终点是否需要反转。
- 此时合法的连通块要么是棵树,要么是棵基环树,这样才能保证存在一个点作为起点,使得整个连通块内的点至多有一个点(起点)被遍历了两次,剩下的点恰好被遍历了一次。
- 当连通块是棵树时,等价于查询至少需要反转多少条边,换根 \(DP\) 维护。
- 设 \(f_{x}\) 表示以 \(1\) 为根时,遍历完以 \(x\) 为根的子树至少需要反转多少条边,状态转移方程为 \(f_{x}=\sum\limits_{y \in Son(x)}f_{y}+w_{x,y}\) 。
- 设 \(g_{x}\) 表示以 \(x\) 为根时,遍历完整棵树至少需要反转多少条边,状态转移方程为 \(\begin{cases} g_{x}=g_{fa_{x}}-1 & w_{fa_{x}}=1 \\ g_{x}=g_{fa_{x}}+1 & w_{fa_{x}}=0 \end{cases}\) 。边界为 \(g_{1}=f_{1}\) 。
- 最终有 \(\min\limits_{i=1}^{n}\{ g_{i} \}\) 即为所求。
- 当连通块是棵基环树时,环上要么是顺时针,要么是逆时针,选择一条边断开后,以两端点进行换根 \(DP\) 即可,此时方案数 \(\in \{ 1,2 \}\) 。
点击查看代码
const ll p=998244353; struct node { int nxt,to,w; }e[200010]; int head[200010],vis[200010],cnt,n_cnt,e_cnt,brokeu,brokev,pos; ll f[200010],g[200010]; vector<ll>s; void add(int u,int v,int w) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; e[cnt].w=w; head[u]=cnt; } void check_dfs(int x) { vis[x]=1; n_cnt++; for(int i=head[x];i!=0;i=e[i].nxt) { e_cnt++; if(vis[e[i].to]==0) { check_dfs(e[i].to); } } } bool check(int n) { for(int i=1;i<=2*n;i++) { if(vis[i]==0) { n_cnt=e_cnt=0; check_dfs(i); if(n_cnt<e_cnt/2)//无向图边数要除以 2 { return false; } } } return true; } void dfs(int x,int fa) { vis[x]=1; for(int i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=fa) { if(vis[e[i].to]==0) { dfs(e[i].to,x); f[x]+=f[e[i].to]+e[i].w; } else { brokeu=x; brokev=e[i].to; pos=i; } } } } void reroot(int x,int fa) { s.push_back(g[x]); for(int i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=fa&&i!=pos&&i!=(pos^1))//因为可能会有重边,所以用边的编号来判断是否在环上 { g[e[i].to]=g[x]+(e[i].w==1?-1:1); reroot(e[i].to,x); } } } int main() { int t,n,u,v,i,j,k; ll ans1,ans2,num; scanf("%d",&t); for(k=1;k<=t;k++) { cnt=1;//从 1 开始 ans1=0; ans2=1; memset(e,0,sizeof(e)); memset(head,0,sizeof(head)); memset(f,0,sizeof(f)); memset(vis,0,sizeof(vis)); scanf("%d",&n); for(i=1;i<=n;i++) { scanf("%d%d",&u,&v); add(u,v,1); add(v,u,0); } if(check(n)==1) { memset(vis,0,sizeof(vis)); for(i=1;i<=2*n;i++) { if(vis[i]==0) { s.clear(); brokeu=brokev=pos=num=0; dfs(i,0); g[i]=f[i]; reroot(i,0); if(brokeu==0) { sort(s.begin(),s.end()); for(j=0;j<s.size();j++) { if(s[j]==s[0]) { num++; } else { break;//略微卡常 } } ans1+=s[0]; ans2=ans2*num%p; } else { num=1+(g[brokeu]+(pos&1)==g[brokev]+((pos&1)^1));//pos&1=1 说明是 w=1 ans1+=min(g[brokeu]+(pos&1),g[brokev]+((pos&1)^1)); ans2=ans2*num%p; } } } printf("%lld %lld\n",ans1,ans2); } else { printf("-1 -1\n"); } } return 0; }
- 考虑建图,从每张牌的正面向反面连一条有向边,那么翻一张牌等价于把边反向。
\(T4\) P174. 单调区间 \(60pts\)
-
部分分
-
\(60pts\) : \(O(n^{2})\) 枚举左右端点,剪枝搜索判断合不合法。
点击查看代码
ll a[200010],b[200010]; vector<ll>s1,s2; bool dfs(ll pos,ll n) { if(pos==n+1) { return true; } else { ll flag=0; if(s1.size()==0||s1[s1.size()-1]>b[pos]) { s1.push_back(b[pos]); flag|=dfs(pos+1,n); s1.pop_back(); } if(s2.size()==0||s2[s2.size()-1]<b[pos]) { s2.push_back(b[pos]); flag|=dfs(pos+1,n); s2.pop_back(); } return flag; } } bool check(ll l,ll r) { for(ll i=l;i<=r;i++) { b[i-l+1]=a[i]; } return dfs(1,r-l+1); } int main() { ll n,ans=0,i,j; cin>>n; for(i=1;i<=n;i++) { cin>>a[i]; } for(i=1;i<=n;i++) { for(j=i;j<=n;j++) { ans+=check(i,j); } } cout<<ans<<endl; return 0; }
-
\(100pts\) :不难发现合法区间具有区间包含单调性,即若 \([L,R]\) 合法,则 \([l,r](L \le l \le r \le R)\) 合法。双指针维护,若不合法则不断移动左指针直至右端点。
点击查看代码
ll a[200010],b[200010]; vector<ll>s1,s2; bool dfs(ll pos,ll n) { if(pos==n+1) { return true; } else { ll flag=0; if(s1.size()==0||s1[s1.size()-1]>b[pos]) { s1.push_back(b[pos]); flag|=dfs(pos+1,n); s1.pop_back(); } if(flag==0&&(s2.size()==0||s2[s2.size()-1]<b[pos])) { s2.push_back(b[pos]); flag|=dfs(pos+1,n); s2.pop_back(); } return flag; } } bool check(ll l,ll r) { for(ll i=l;i<=r;i++) { b[i-l+1]=a[i]; } return dfs(1,r-l+1); } int main() { ll n,ans=0,i,l,r; cin>>n; for(i=1;i<=n;i++) { cin>>a[i]; } for(l=r=1;r<=n;r++) { while(l<=r&&check(l,r)==0) { l++; } ans+=r-l+1; } cout<<ans<<endl; return 0; }
-
-
正解
- 判断一段区间合不合法同 CF1144G Two Merged Sequences 。
- 仍考虑利用区间包含单调性的性质,对 \([l,r]\) 进行分治。
- 设 \(ed_{i}\) 表示 \(i\) 向右所能延伸到的最大端点,先求出 \(ed_{l},ed_{r}\) ,若 \(ed_{l}=ed_{r}\) 则有 \(\forall i \in (l,r),ed_{i}=ed_{l}\) ,否则递归处理 \([l,mid]\) 和 \([mid+1,r]\) 。
- 时间复杂度是正确的证明和从右往左扫基本一样,但我都看不懂。
- 最终,有 \(\sum\limits_{i=1}^{n}(ed_{i}-i+1)\) 即为所求。
点击查看代码
ll a[200010],f[200010][2],ed[200010]; ll dp(ll x,ll n) { f[x][0]=0x7f7f7f7f; f[x][1]=-0x7f7f7f7f; for(ll i=x+1;i<=n;i++) { f[i][0]=-0x7f7f7f7f; f[i][1]=0x7f7f7f7f; if(a[i-1]<a[i]) { f[i][0]=max(f[i][0],f[i-1][0]); } if(f[i-1][1]<a[i]) { f[i][0]=max(f[i][0],a[i-1]); } if(a[i-1]>a[i]) { f[i][1]=min(f[i][1],f[i-1][1]); } if(f[i-1][0]>a[i]) { f[i][1]=min(f[i][1],a[i-1]); } if(f[i][0]==-0x7f7f7f7f&&f[i][1]==0x7f7f7f7f) { return i-1; } } return n; } void solve(ll l,ll r,ll n) { ed[l]=(ed[l]==0)?dp(l,n):ed[l]; ed[r]=(ed[r]==0)?dp(r,n):ed[r]; if(l==r) { return; } ll mid=(l+r)/2; if(ed[l]==ed[r]) { for(ll i=l+1;i<=r-1;i++) { ed[i]=ed[l]; } } else { solve(l,mid,n); solve(mid+1,r,n); } } int main() { ll n,ans=0,i; cin>>n; for(i=1;i<=n;i++) { cin>>a[i]; } solve(1,n,n); for(i=1;i<=n;i++) { ans+=ed[i]-i+1; } cout<<ans<<endl; return 0; }
总结
- \(T1\) 赛时把结论猜出来后一直在想怎么处理边界和怎么进行 \(hack\) ,到最后也没打代码。
- \(T2\) 因为提交前忘再编译一遍了,把分号打成了冒号,因 \(CE\) 挂了 \(23pts\) ;因为知道是板子,但自己没学所以干脆没去想 \(Dijkstra\) 怎么求解最小环,可能是学的东西太多了导致的(?)。
后记
- 失恋三部曲。
- 原 \(T1\) 那一天她与我许下约定
那一天我们在教室里许下约定。
我至今还记得我们许下约定时的欢声笑语。我记得她说过她喜欢吃饼干,很在意自己体重的同时又控制不住自己。她跟我做好了约定:我拿走她所有的饼干共 N 块,在从今天起不超过 D 天的时间里把所有的饼干分次给她,每天给她的饼干数要少于 M 以防止她吃太多。
当然,我们的约定并不是饼干的约定,而是一些不可言状之物。
现今回想这些,我突然想知道,有多少种方案来把饼干分给我的她。
- 现 \(T2\) 那一天她离我而去
她走的悄无声息,消失的无影无踪。
至今我还记得那一段时间,我们一起旅游,一起游遍山水。到了最终的景点,她却悄无声息地消失了,只剩我孤身而返。
现在我还记得,那个旅游区可以表示为一张由 n 个节点 m 条边组成无向图。我故地重游,却发现自己只想尽快地结束这次旅游。我从景区的出发点(即 1 号节点)出发,却只想找出最短的一条回路重新回到出发点,并且中途不重复经过任意一条边。
即:我想找出从出发点到出发点的小环。
- 现 \(T3\) 哪一天她能重回我身边
她依然在我不知道的地方做我不知道的事。
桌面上摊开着一些卡牌,这是她平时很爱玩的一个游戏。如今卡牌还在,她却不在我身边。不知不觉,我翻开了卡牌,回忆起了当时一起玩卡牌的那段时间。
每张卡牌的正面与反面都各有一个数字,我每次把卡牌按照我想的放到桌子上,一共 n 张,而她则是将其中的一些卡牌翻转,最后使得桌面上所有朝上的数字都各不相同。
我望着自己不知不觉翻开的卡牌,突然想起了之前她曾不止一次的让我帮她计算最少达成目标所需要的最少的翻转次数,以及最少翻转达成目标的方案数。(两种方式被认为是相同的当且仅当两种方式需要翻转的卡牌的集合相同)
如果我把求解过程写成程序发给她,她以后玩这个游戏的时候会不会更顺心一些?
- 原 \(T1\) 那一天她与我许下约定
- \(T4\) 的样例贺假了。
本文来自博客园,作者:hzoi_Shadow,原文链接:https://www.cnblogs.com/The-Shadow-Dragon/p/18355161,未经允许严禁转载。
版权声明:本作品采用 「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0) 进行许可。