CodeForces 1385 - Codeforces Round #656 (Div. 3)
今天下午看到有7個題,打了個vp,一開始準備用chenxiaoyan打的,然後調錯時間了,無奈只能用小號WinnieThePooh打/xk
最終還剩幾分鐘的時候AK了/cy
吐槽一下,這場咋全多測啊?
CF比賽頁面傳送門
A - Three Pairwise Maximums
洛谷還沒爬,下同,管理員快爪巴 & CF題目頁面傳送門
題意紫帆。
考慮將\(a,b,c\)從小到大排序,則\(x=b,y=z=c\)。將題目給出的\(x,y,z\)排序之後,若\(y\neq z\)則無解,否則\(a=1,b=x,c=y\)是一組解。
程式碼:
#include<bits/stdc++.h> using namespace std; void mian(){ int x,y,z; scanf("%d%d%d",&x,&y,&z); if(x>y)swap(x,y);if(y>z)swap(y,z); if(x>y)swap(x,y);if(y>z)swap(y,z); if(y!=z)return puts("NO"),void(); puts("YES"); cout<<1<<" "<<x<<" "<<y<<"\n"; } int main(){ int testnum; cin>>testnum; while(testnum--)mian(); return 0; }
B - Restore the Permutation by Merger
CF題目頁面傳送門
題意紫帆。
把\(1\sim n\)所有數以第一次出現的位置取下來,相對位置不變,組成一個排列,就是答案。自證不難。
程式碼:
#include<bits/stdc++.h> using namespace std; const int N=50; int n; bool hav[N+1]; void mian(){ cin>>n; memset(hav,0,sizeof(hav)); for(int i=1;i<=2*n;i++){ int x; scanf("%d",&x); if(!hav[x])hav[x]=true,printf("%d ",x); } puts(""); } int main(){ int testnum; cin>>testnum; while(testnum--)mian(); return 0; }
C - Make It Good
CF題目頁面傳送門
題意紫帆。
不難發現一個數列是好的當且僅當它非嚴格單峰。自證不難。又發現,刪掉字首相當於留下字尾(這個出題人迷惑的太失敗了),右端點是不變的。於是貪心,從右往左找到最左邊的一個可以當峰值的位置,然後再從峰值往左邊找到最左邊的左端點。最終答案就是左端點減一。
程式碼:
#include<bits/stdc++.h> using namespace std; const int N=200000; int n; int a[N+1]; void mian(){ cin>>n; for(int i=1;i<=n;i++)scanf("%d",a+i); int las; for(int i=n;i;i--){//找最左峰值 if(i<n&&a[i]<a[i+1])break; las=i; } int las0; for(int i=las;i;i--){//找最左左端點 if(i<las&&a[i]>a[i+1])break; las0=i; } cout<<las0-1<<"\n"; } int main(){ int testnum; cin>>testnum; while(testnum--)mian(); return 0; }
D - a-Good String
CF題目頁面傳送門
題意紫帆。
做法很顯然,考慮DP,設\(dp_{l,r,x}\)表示子串\(s_{l\sim r}\)成為\(x\)-good string所需要的最小運算元。邊界:\(dp_{l,l,x}=[a_l\neq x]\),目標:\(dp_{1,n,\texttt a}\),轉移:\(dp_{l,r,x}=\min\left(\sum\limits_{i=l}^{\frac{l+r-1}2}[a_i\neq x]+dp_{\frac{l+r-1}2+1,r,x+1},\sum\limits_{i=\frac{l+r-1}2+1}^{r}[a_i\neq x]+dp_{l,\frac{l+r-1}2,x+1}\right)\)。
然而這樣看起來空間是\(\mathrm O\!\left(n^2\log n\right)\)的,其實合法的\((l,r)\)對只有\(\mathrm O(n)\)個,跟線段樹類似,而且對於每個合法的\((l,r)\)都只有一個合法的\(x\)對應。所以空間複雜度\(\mathrm O(n)\),時間複雜度類似歸併樹,是\(\mathrm O\!\left(\sum n\log n\right)\)的。然鵝現場我沒有考慮到,於是空間開了\(\mathrm O(n\log n)\),清空陣列的時候也花了這麼多。不管了。
程式碼:
#include<bits/stdc++.h>
using namespace std;
const int N=200000,LET=20;
int n;
char a[N+5];
int dp[N<<2][LET];
int dfs(int l=1,int r=n,int c=0,int p=1){//記憶化搜尋
if(l==r)return a[l]!='a'+c;
if(~dp[p][c])return dp[p][c];
int &res=dp[p][c],mid=l+r>>1,sum1=0,sum2=0;
for(int i=l;i<=mid;i++)sum1+=a[i]!='a'+c;
for(int i=mid+1;i<=r;i++)sum2+=a[i]!='a'+c;
res=min(sum1+dfs(mid+1,r,c+1,p<<1|1),sum2+dfs(l,mid,c+1,p<<1));//轉移方程
// printf("dp[%d][%d][%d]=%d\n",l,r,c,res);
return res;
}
void mian(){
cin>>n;
scanf("%s",a+1);
for(int i=1;i<=4*n;i++)for(int j=0;j<LET;j++)dp[i][j]=-1;//清空
cout<<dfs()<<"\n";
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
E - Directing Edges
CF題目頁面傳送門
有一張\(n\)個點\(m\)條邊的混合圖,你需要給所有無向邊定向,使得最終得到的有向圖無環。或報告無解。本題多測。
\(\sum n\in[2,2\times10^5],\sum m\in[1,2\times10^5]\)。
事實證明,這是本場最難的一題,因為我到最後才想出來
首先注意到,若不考慮所有無向邊,剩下來的有向圖是有環的,那顯然無解。
然後呢?當時順序做到這題的時候我想了DFS,Tarjan縮點(這個一年沒寫了,我已經不會了),都沒有思路,卻沒有考慮到有向圖上經常用到的拓撲排序(最後才想到)。考慮對不考慮所有無向邊得到的有向圖拓撲排序,那麼無向邊的定向方案很容易構造:拓撲序小的連向拓撲序大的即可保證無環。
程式碼:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=200000;
int n,m;
vector<pair<int,int> > nei[N+1];
int ideg[N+1];
vector<int> topo;
int id[N+1];
void toposort(){//拓撲排序
topo.clear();
queue<int> q;
for(int i=1;i<=n;i++)if(!ideg[i])q.push(i);
while(q.size()){
int x=q.front();
q.pop();
topo.pb(x);
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i].X,z=nei[x][i].Y;
if(z)/*不考慮無向邊*/if(!--ideg[y])q.push(y);
}
}
}
void mian(){
cin>>n>>m;
for(int i=1;i<=n;i++)ideg[i]=0,nei[i].clear();
while(m--){
int x,y,z;
scanf("%d%d%d",&z,&x,&y);
if(z)nei[x].pb(mp(y,z)),ideg[y]++;
else nei[x].pb(mp(y,z)),nei[y].pb(mp(x,z));
}
toposort();
if(topo.size()!=n)return puts("NO"),void();//無解
puts("YES");
for(int i=0;i<n;i++)id[topo[i]]=i;
for(int i=1;i<=n;i++)for(int j=0;j<nei[i].size();j++){
int x=nei[i][j].X,y=nei[i][j].Y;
if(y)printf("%d %d\n",i,x);
else if(id[i]<id[x])printf("%d %d\n",i,x);//拓撲序小的連向拓撲序大的
}
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
F - Removing Leaves
CF題目頁面傳送門
給定一棵無根樹\(T=(V,E),|V|=n,|E|=n-1\)和一個常數\(m\)。每次可以選擇恰好\(m\)個唯一連線的節點相同的葉子並刪除。求最多能刪多少次。本題多測。
\(\sum n\in\left[1,2\times 10^5\right]\)。
我記得我在這篇裡吐槽過打的3次D3F全是樹形DP?這次樹形DP喜加一。
注意到,對於任何一種刪的方案,最終必定會有至少一個節點留下來刪不掉。我們可以欽定這個點為根樹形DP,最後二次掃描。接下來考慮如何DP。
設\(dp0_i\)表示子樹\(i\)是否能刪得只剩一個\(i\),\(dp_i\)表示子樹\(i\)內最多能刪幾次。轉移挺簡單的,設\(cnt_i=\sum\limits_{j\in son_i}dp0_j\),則
\[\begin{cases}dp0_i=[cnt_i=|son_i|][m\mid cnt_i]\\dp_i=\sum_{j\in son_i}dp_j+\left\lfloor\dfrac{cnt_i}m\right\rfloor\end{cases} \]
接下來就愉快地做出來了。
然而現場我nt了。我就懶得寫嚴格\(\mathrm O(1)\)的換根,寫了個calc
函式計算DP值(\(\mathrm O(|son_x|)\)),然後換根的時候呼叫這個函式。我當時zz地認為這樣是均攤\(\mathrm O(1)\)的,交上去,TLE13。我就很生氣,CF啥時候也卡常了?就算卡,這也要卡?於是吸臭氧碼讀優還是T。無奈之下只好改成嚴格\(\mathrm O(1)\)的換根,這樣還需要記錄\(cnt\)陣列。然後就A了。賽後才發現一個菊花圖就能把我卡沒了。。。。。
程式碼:
#pragma GCC optimize(3)///xk
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
void read(int &x){///xk
x=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
}
void prt(int x){///xk
if(x>9)prt(x/10);
putchar(x%10^48);
}
const int N=200000;
int n,m;
vector<int> nei[N+1];
int cnt[N+1];
bool dp0[N+1];
int dp[N+1];
void dfs(int x=1,int fa=0){//初DP
cnt[x]=0;dp0[x]=true;dp[x]=0;
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i];
if(y==fa)continue;
dfs(y,x);
cnt[x]+=dp0[y];dp0[x]&=dp0[y];dp[x]+=dp[y];
}
dp[x]+=cnt[x]/m;
dp0[x]&=cnt[x]%m==0;
// printf("dp[%d]=%d\n",x,dp[x]);
}
int ans;
void dfs0(int x=1,int fa=0){//二次掃描
// printf("%d=%d\n",x,dp[x]);
ans=max(ans,dp[x]);//更新答案
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i];
if(y==fa)continue;
int cnt_x=cnt[x],dp0_x=dp0[x],dp_x=dp[x],cnt_y=cnt[y],dp0_y=dp0[y],dp_y=dp[y];
cnt[x]-=dp0[y];dp0[x]=cnt[x]==nei[x].size()-1&&cnt[x]%m==0;dp[x]=dp[x]-dp[y]-(cnt[x]+dp0[y])/m+cnt[x]/m;
cnt[y]+=dp0[x];dp0[y]=cnt[y]==nei[y].size()&&cnt[y]%m==0;dp[y]=dp[y]+dp[x]-(cnt[y]-dp0[x])/m+cnt[y]/m;//換根
dfs0(y,x);
cnt[x]=cnt_x;dp0[x]=dp0_x;dp[x]=dp_x;cnt[y]=cnt_y;dp0[y]=dp0_y;dp[y]=dp_y;//還原
}
}
void mian(){
read(n);read(m);
for(int i=1;i<=n;i++)nei[i].clear();
for(int i=1;i<n;i++){
int x,y;
read(x);read(y);
nei[x].pb(y);nei[y].pb(x);
}
dfs();
ans=0;dfs0();
prt(ans);putchar('\n');
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}
G - Columns Swaps
CF題目頁面傳送門
有一個\(2\times n\)的矩陣\(a\),每個數在\([1,n]\)內。要求若干次交換某一列的\(2\)個值,使得最後每行都是\(1\sim n\)的排列。求最小次數以及對應方案,或報告無解。本題多測。
\(\sum n\in[1,2\times 10^5]\)。
首先,每個數出現的次數必須要是\(2\),否則無解。
然後。看到排列想到圖論。不難發現結論:若\(\forall i\in[1,n]\),連有向邊\((a_{1,i},a_{2,i})\),得到的圖可以表示為一個排列當且僅當上下都是排列。證明的話,必要性可以用置換的乘積證,充分性xjb隨便證即可(就是每個點入出度都為\(1\),則在上下各出現過一次)。
那麼,原操作相當於將一條邊反向。考慮將原\(a\)的圖建出來,不考慮方向依次考慮每個CC(環),然後可以整體調成\(2\)種方向,比個大小即可。對於CC大小為\(1\)和\(2\)需要討論一下,有點煩?(當時差點討論絕望了)
程式碼:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=200000;
int n;
int a[N+1],b[N+1];
vector<pair<int,pair<int,int> > > nei[N+1];
int cnt[N+1];
bool vis[N+1];
vector<int> zero,one;
int st,to;
void dfs(int x){//找環
vis[x]=true;
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i].X,z=nei[x][i].Y.X,xx=nei[x][i].Y.Y;
if(!vis[y]){
if(x==st)to=y;
dfs(y);
if(xx)one.pb(z);else zero.pb(z);
}
}
}
void mian(){
scanf("%d",&n);
for(int i=1;i<=n;i++)cnt[i]=0,nei[i].clear(),vis[i]=false;
for(int i=1;i<=n;i++)scanf("%d",a+i),cnt[a[i]]++;
for(int i=1;i<=n;i++)scanf("%d",b+i),cnt[b[i]]++;
for(int i=1;i<=n;i++)if(cnt[i]!=2)return puts("-1"),void();//判無解
for(int i=1;i<=n;i++)nei[a[i]].pb(mp(b[i],mp(i,1))),nei[b[i]].pb(mp(a[i],mp(i,0)));//建圖
vector<int> ans;
for(int i=1;i<=n;i++)if(!vis[i]){
one.clear(),zero.clear(),st=i,dfs(i);
if(one.empty()&&zero.empty())continue;//大小為1
if(nei[st][0].X==nei[st][1].X){//大小為2
if(nei[st][0].Y.Y==nei[st][1].Y.Y)ans.pb(nei[st][0].Y.X);
continue;
}
int id=nei[st][0].X==to?1:0;
if(nei[st][id].Y.Y)zero.pb(nei[st][id].Y.X);else one.pb(nei[st][id].Y.X);
if(one.size()<zero.size())for(int i=0;i<one.size();i++)ans.pb(one[i]);
else for(int i=0;i<zero.size();i++)ans.pb(zero[i]);//比大小壓進答案序列
}
printf("%d\n",int(ans.size()));
for(int i=0;i<ans.size();i++)printf("%d ",ans[i]);
puts("");
}
int main(){
int testnum;
cin>>testnum;
while(testnum--)mian();
return 0;
}