1. 程式人生 > 實用技巧 >CodeForces 1385 - Codeforces Round #656 (Div. 3)

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;
}