CSP2019 題解

Day1T1 格雷碼

這道題比較送分
考慮格雷碼的生成方式
實際上每次如果下一位是1就和下一位是0的反轉一下
考慮再下一位,如果還是1就會再反轉一下,如果兩位是0或兩位是1就會變回原狀,否則就會剛好反轉,所以只會和前後兩位有關,推一下就可以知道格雷碼其實是x(x>>1)x \oplus (x>>1)
考場上沒推到x(x>>1)x \oplus (x>>1),但是也可以寫,影響不大。
代碼:

#include<cstdio>
#include<algorithm>
using namespace std;
typedef unsigned long long ull;
int n,rev,ans[100];
ull k;
int main(){
	scanf("%d%llu",&n,&k);
	while(n--){
		if(k<(1ull<<n)){
			printf("%d",rev);
			rev=0;
		}
		else{
			printf("%d",rev^1);
			rev=1;
			k-=(1ull<<n);
		}
	}
	return 0;
}

Day1T2 括號樹
對於每一個點,考慮以這個點爲末尾增加了多少個合法括號串,如果是左括號就是0,如果是右括號可以用棧找到當前點匹配的括號作爲一個,再加上匹配點的父親產生的合法括號串的個數,最後每個點再算鏈上的前綴和。
代碼:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=500010;
typedef long long ll;
int n,tp,tot,f[N],head[N],to[N],nxt[N],stk[N],val[N];
ll dp[N],ans;
void add_edge(int u,int v){
	nxt[++tot]=head[u];
	to[tot]=v;
	head[u]=tot;
	return;
}
void dfs(int u){
	int x=0;
	if(tp>0&&val[stk[tp]]==0&&val[u]==1){
		x=stk[tp--];
		dp[u]+=dp[f[x]]+1;
	}
	else stk[++tp]=u;
	for(int i=head[u];~i;i=nxt[i])
		dfs(to[i]);
	if(x)
		stk[++tp]=x;
	else tp--;
	return;
}
void dfs2(int u){
	ans^=dp[u]*u;
	for(int i=head[u];~i;i=nxt[i]){
		dp[to[i]]+=dp[u];
		dfs2(to[i]);
	}
	return;
}
int main(){
	memset(head,-1,sizeof(head));
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		char c;
		do c=getchar();
		while(c!='('&&c!=')');
		if(c=='(')
			val[i]=0;
		else val[i]=1;
	}
	for(int i=2;i<=n;i++){
		scanf("%d",f+i);
		add_edge(f[i],i);
	}
	dfs(1);
	dfs2(1);
	printf("%lld\n",ans);
	return 0;
}

Day1T3 樹上的數
考慮貪心,對於儘量小的數應該移到儘量小的點上,會發現這樣就可以產生一個邊的次序,而且邊的次序只對於一個點的所有連邊有這樣的順序。如果一個數要從s點換到t點,次序大概是這樣的:
對於s點,這條路經上的那條邊一定是最先刪除的
對於t點,這條路徑上的那條邊一定是最後刪除的
對於路徑上的點,這條路徑上的那兩條邊一定是按順序緊接着刪除的
於是我們可以用一個鏈表維護每個點所連邊之間的順序,從小到大枚舉每個數,dfs找到可以到達的最小點
具體怎麼維護每個點連邊的順序,我們可以先對所有所連邊建一個鏈表,在添加一個次序的時候合併兩個鏈表。爲了方便維護,因爲在鏈表中間的邊顯然不會再連邊,所以可以將nxt和pre直接設爲-1,如果這條邊是第一條刪除的邊,可以把pre設爲0,最後一條邊同理。
連邊和判斷能否走到下一個點具體的可以看代碼實現,有點難講。
代碼:

#include<vector>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2010;
int T,n,tot,mini,fa[N],a[N],deg[N],head[N],to[N*N],nxt[N*N],pre[N][N],nt[N][N];
void add_edge(int u,int v){
	nxt[++tot]=head[u];
	to[tot]=v;
	head[u]=tot;
	return;
}
bool check(int u,int l,int r){
	if(pre[u][l]==-1||nt[u][r]==-1||nt[u][r]==l||(pre[u][l]==0&&nt[u][r]==n+1&&deg[u]!=2))
		return 0;
	return 1;
}
void dfs(int u){
	if(fa[u]&&check(u,fa[u],n+1))
		mini=min(mini,u);
	for(int i=head[u];~i;i=nxt[i]){
		int v=to[i];
		if(v==fa[u]||!check(u,fa[u],v))
			continue;
		fa[v]=u;
		dfs(v);
	}
	return;
}
void merge(int u,int l,int r){
	nt[u][pre[u][l]]=nt[u][r];
	pre[u][nt[u][r]]=pre[u][l];
	nt[u][r]=pre[u][l]=-1;
	deg[u]--;
	return;
}
void solve(int x){
	merge(x,fa[x],n+1);
	while(fa[fa[x]]){
		int tmp=x;
		x=fa[x];
		merge(x,fa[x],tmp);
	}
	int tmp=x;
	x=fa[x];
	merge(x,0,tmp);
	return;
}
int main(){
	scanf("%d",&T);
	while(T--){
		tot=0;
		memset(head,-1,sizeof(head));
		scanf("%d",&n);
		for(int i=1;i<=n;i++){
			scanf("%d",a+i);
			deg[i]=2;
		}
		for(int i=1;i<n;i++){
			int u,v;
			scanf("%d%d",&u,&v);
			add_edge(u,v);
			add_edge(v,u);
			deg[u]++;
			deg[v]++;
		}
		for(int i=1;i<=n;i++)
			for(int j=0;j<=n+1;j++)
				pre[i][j]=nt[i][j]=j;
		for(int i=1;i<=n;i++){
			mini=0x7f7f7f7f;
			fa[a[i]]=0;
			dfs(a[i]);
			solve(mini);
			printf("%d ",mini);
		}
		putchar('\n');
	}
	return 0;
}

Day2T1 Emiya 家今天的飯
首先顯然做的菜的數量小於等於會的烹飪方法數
考慮到直接求比較複雜,先不考慮每種食材的出現次數,求出總方案數,顯然可以一個dp或者數學推一下就出來了。
這裏給出dp的方法:
dp[i][j]dp[i][j]表示前i個烹飪方法中做了j道菜的方案數,則有:
dp[i][j]=dp[i1][j]+dp[i1][j1]sum[i]dp[i][j]=dp[i-1][j]+dp[i-1][j-1]*sum[i]
其中sum[i]sum[i]表示第i種烹飪方式中可以做的菜的總數。
再考慮題目的限制,顯然最多隻有一種主要食材在超過一半的菜中出現,所以我們可以直接枚舉主要食材,然後求方案數之後從總方案數減去就ok了。
怎麼在已知一個在超過一半的菜的主要食材時求出方案數呢?
考慮一個樸素的dp,f[i][j][k]f[i][j][k]表示前i種烹飪方法中做了j道菜,其中有k道菜是用已知食材做的。
那麼就可以得出:
f[i][j][k]=(f[i1][j][k]+f[i1][j1][k](sum[i]a[i][now])+f[i1][j][k1]a[i][now])f[i][j][k]=(f[i-1][j][k]+f[i-1][j-1][k]*(sum[i]-a[i][now])+f[i-1][j][k-1]*a[i][now])
其中now表示當前已知食材。
於是就有了O(mn3)O(mn^3)的84分優秀做法。
注意到最終的狀態只有極小部分的是需要用到了,那麼是不是可以考慮一下壓縮狀態呢?
我們會發現用當前已知食材的菜超過一半,會發現這樣就會比其它所有菜的數量加起來要多。
所以我們可以把dp的後兩維壓成一維,表示當前已知食材做的菜的數量和其它食材做的菜的差,於是就有了dp方程:
f[i][j]=f[i1][j]+f[i1][j1]a[i][now]+f[i1][j+1](sum[i]a[i][now])f[i][j]=f[i-1][j]+f[i-1][j-1]*a[i][now]+f[i-1][j+1]*(sum[i]-a[i][now])
代碼:

#include<cstdio>
#include<algorithm>
using namespace std;
const int N=110,M=2010;
const int mod=998244353;
int n,m,ans,a[N][M],sum[N],dp[N][N],g[N][N*2],*f[N];
int Add(int a,int b){
	return a+b>=mod?a+b-mod:a+b;
}
int Minus(int a,int b){
	return a<b?a-b+mod:a-b;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			scanf("%d",a[i]+j);
			sum[i]=Add(sum[i],a[i][j]);
		}
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		dp[i][0]=dp[i-1][0];
		for(int j=1;j<=n;j++)
			dp[i][j]=(dp[i-1][j]+1ll*dp[i-1][j-1]*sum[i])%mod;
	}
	for(int i=1;i<=n;i++)
		ans=Add(ans,dp[n][i]);
	for(int i=0;i<=n;i++)
		f[i]=g[i]+N;
	for(int now=1;now<=m;now++){
		f[0][0]=1;
		for(int i=1;i<=n;i++)
			for(int j=-n;j<=n;j++)
				f[i][j]=Add(f[i-1][j],Add(1ll*f[i-1][j-1]*a[i][now]%mod,1ll*f[i-1][j+1]*Minus(sum[i],a[i][now])%mod));
		for(int i=1;i<=n;i++)
			ans=Minus(ans,f[n][i]);
	}
	printf("%d",ans);
	return 0;
}

Day2T2
這道題有個的結論:最優解的最後一段會盡量小
證明的話可以用數學歸納法:
dp[m]dp[m]表示前m項的答案,sum[m]sum[m]表示前m項的和,最後一段最小時左端點爲jj,最後一段存在的左端點爲kk
所以問題轉化爲證明不等式:
dp[i]=dp[j]+(sum[i]sum[j])2dp[k]+(sum[i]sum[k])2dp[i]=dp[j]+(sum[i]-sum[j])^2\le dp[k]+(sum[i]-sum[k])^2
i=0就不說了
記當前所求的是前i項的答案
假設mmmm<ii時原命題成立,
考慮作差法證明不等式:
dp[j]+(sum[i]sum[j])2dp[k](sum[i]sum[k])2dp[j]+(sum[i]-sum[j])^2-dp[k]-(sum[i]-sum[k])^2
=dp[j]dp[k]+(2sum[i]sum[k]sum[j])(sum[k]sum[j])=dp[j]-dp[k]+(2sum[i]-sum[k]-sum[j])(sum[k]-sum[j])
dp[j]dp[k](sum[j]sum[k])2\le dp[j]-dp[k]-(sum[j]-sum[k])^2
dp[j]dp[k]+(sum[j]sum[k])2\because dp[j]\le dp[k]+(sum[j]-sum[k])^2
dp[j]dp[k](sum[j]sum[k])20\therefore dp[j]-dp[k]-(sum[j]-sum[k])^2\le0
dp[j]+(sum[i]sum[j])2dp[k](sum[i]sum[k])20\therefore dp[j]+(sum[i]-sum[j])^2-dp[k]-(sum[i]-sum[k])^2\le0
原命題得證。
有了這個結論就可以直接一個單調隊列求出最後一段的左端點,然後直接算答案就好了。
代碼:

#include<cstdio>
#include<algorithm>
using namespace std;
const int N=40000010,base=1e9;
typedef long long ll;
int n,type,hd,tl,q[N];
ll a[N],f[N];
struct data{
	int a[4];
	data(){
		a[0]=a[1]=a[2]=a[3]=0;
	}
	int& operator[](int x){
		return a[x];
	}
	friend data operator+(data x,data y){
		ll ret[4];
		for(int i=0;i<4;i++)
			ret[i]=x[i]+y[i];
		for(int i=0;i<3;i++){
			ret[i+1]+=ret[i]/base;
			ret[i]%=base;
		}
		data ans;
		for(int i=0;i<4;i++)
			ans[i]=ret[i];
		return ans;
	}
	void write(){
		bool ck=0;
		for(int i=3;i>=0;i--)
			if(a[i]&&!ck){
				ck=1;
				printf("%d",a[i]);
			}
			else if(ck)
				printf("%09d",a[i]);
		return;
	}
};
data Sqr(ll x){
	ll tmp[4]={0,0,0,0};
	tmp[0]=x%base;
	tmp[1]=x/base;
	tmp[2]=tmp[1]*tmp[1];
	tmp[1]=tmp[0]*tmp[1]*2;
	tmp[0]=tmp[0]*tmp[0];
	for(int i=0;i<3;i++){
		tmp[i+1]+=tmp[i]/base;
		tmp[i]%=base;
	}
	data ans;
	for(int i=0;i<4;i++)
		ans[i]=tmp[i];
	return ans;
}
void rd0(){
	for(int i=1;i<=n;i++)
		scanf("%d",a+i);
	return;
}
void rd1(){
	int x,y,z,m,p,l,r,lstp=0;
	scanf("%d%d%d%d%d%d",&x,&y,&z,a+1,a+2,&m);
	for(int i=3;i<=n;i++)
		a[i]=(1ll*x*a[i-1]+1ll*y*a[i-2]+z)&((1<<30)-1);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&p,&l,&r);
		p=min(p,n);
		for(int j=lstp+1;j<=p;j++)
			a[j]=a[j]%(r-l+1)+l;
		lstp=p;
	}
	return;
}
int main(){
	scanf("%d%d",&n,&type);
	if(type)
		rd1();
	else
		rd0();
	for(int i=1;i<=n;i++){
		a[i]+=a[i-1];
		while(hd<tl&&a[q[hd+1]]-a[f[q[hd+1]]]<=a[i]-a[q[hd+1]])
			hd++;
		f[i]=q[hd];
		while(hd<tl&&a[q[tl]]-a[f[q[tl]]]+a[q[tl]]>a[i]-a[f[i]]+a[i])
			tl--;
		q[++tl]=i;
	}
	data ans;
	for(int x=n;x;x=f[x])
		ans=ans+Sqr(a[x]-a[f[x]]);
	ans.write();
	return 0;
}

Day2T3 樹的重心
直接在dfs的時候一邊換爲以x爲根,然後會發現一棵樹的重心會在所有重鏈的交集上,維護一個倍增跳重兒子找重心,如果有另一個重心一定是父親或者重兒子,直接判就行。
怎麼維護重兒子可以見代碼。
代碼:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=300010;
typedef long long ll;
int T,n,tot,head[N],to[N*2],nxt[N*2],f[N],siz[N],son[N][20],secson[N];
ll ans;
void add_edge(int u,int v){
	nxt[++tot]=head[u];
	to[tot]=v;
	head[u]=tot;
	return;
}
void dfs1(int u){
	siz[u]=1;
	for(int i=head[u];~i;i=nxt[i]){
		int v=to[i];
		if(v==f[u])
			continue;
		f[v]=u;
		dfs1(v);
		if(siz[v]>siz[son[u][0]]){
			secson[u]=son[u][0];
			son[u][0]=v;
		}
		else if(siz[v]>siz[secson[u]])
			secson[u]=v;
		siz[u]+=siz[v];
	}
	for(int i=1;i<=18;i++)
		son[u][i]=son[son[u][i-1]][i-1];
	return;
}
void dfs2(int u,int father){
	int s=son[u][0],sc=secson[u],sz=siz[u];
	for(int i=head[u];~i;i=nxt[i]){
		int v=to[i];
		if(v==father)
			continue;
		int x=v;
		for(int i=17;i>=0;i--)
			if(siz[v]-siz[son[x][i]]<=siz[v]/2)
				x=son[x][i];
		if(max(siz[v]-siz[x],siz[son[x][0]])<=siz[v]/2)
			ans+=x;
		if(max(siz[v]-siz[son[x][0]],siz[son[son[x][0]][0]])<=siz[v]/2)
			ans+=son[x][0];
		if(max(siz[v]-siz[f[x]],siz[son[f[x]][0]])<=siz[v]/2)
			ans+=f[x];
		siz[u]=n-siz[v];
		son[u][0]=father;
		f[u]=0;
		if(s!=v)
			if(siz[son[u][0]]<siz[s])
				son[u][0]=s;
		if(sc!=v)
			if(siz[son[u][0]]<siz[sc])
				son[u][0]=sc;
		for(int i=1;i<=17;i++)
			son[u][i]=son[son[u][i-1]][i-1];
		x=u;
		for(int i=17;i>=0;i--)
			if(siz[u]-siz[son[x][i]]<=siz[u]/2)
				x=son[x][i];
		if(max(siz[u]-siz[x],siz[son[x][0]])<=siz[u]/2)
			ans+=x;
		if(max(siz[u]-siz[son[x][0]],siz[son[son[x][0]][0]])<=siz[u]/2)
			ans+=son[x][0];
		if(max(siz[u]-siz[f[x]],siz[son[f[x]][0]])<=siz[u]/2)
			ans+=f[x];
		f[u]=v;
		dfs2(v,u);
	}
	f[u]=father;
	siz[u]=sz;
	secson[u]=sc;
	son[u][0]=s;
	for(int i=1;i<=17;i++)
		son[u][i]=son[son[u][i-1]][i-1];
	return;
}
int main(){
	scanf("%d",&T);
	while(T--){
		tot=ans=0;
		memset(head,-1,sizeof(head));
		memset(son,0,sizeof(son));
		memset(secson,0,sizeof(secson));
		scanf("%d",&n);
		for(int i=1;i<n;i++){
			int u,v;
			scanf("%d%d",&u,&v);
			add_edge(u,v);
			add_edge(v,u);
		}
		dfs1(1);
		dfs2(1,0);
		printf("%lld\n",ans);
	}
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章