CSP2019 题解

Day1T1 格雷码

这道题比较送分
考虑格雷码的生成方式
实际上每次如果下一位是1就和下一位是0的反转一下
考虑再下一位,如果还是1就会再反转一下,如果两位是0或两位是1就会变回原状,否则就会刚好反转,所以只会和前后两位有关,推一下就可以知道格雷码其实是 x ⊕ ( x > > 1 ) x \oplus (x>>1) x(x>>1)
考场上没推到 x ⊕ ( x > > 1 ) x \oplus (x>>1) x(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的方法:
d p [ i ] [ j ] dp[i][j] dp[i][j]表示前i个烹饪方法中做了j道菜的方案数,则有:
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − 1 ] ∗ s u m [ i ] dp[i][j]=dp[i-1][j]+dp[i-1][j-1]*sum[i] dp[i][j]=dp[i1][j]+dp[i1][j1]sum[i]
其中 s u m [ i ] sum[i] sum[i]表示第i种烹饪方式中可以做的菜的总数。
再考虑题目的限制,显然最多只有一种主要食材在超过一半的菜中出现,所以我们可以直接枚举主要食材,然后求方案数之后从总方案数减去就ok了。
怎么在已知一个在超过一半的菜的主要食材时求出方案数呢?
考虑一个朴素的dp, f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示前i种烹饪方法中做了j道菜,其中有k道菜是用已知食材做的。
那么就可以得出:
f [ i ] [ j ] [ k ] = ( f [ i − 1 ] [ j ] [ k ] + f [ i − 1 ] [ j − 1 ] [ k ] ∗ ( s u m [ i ] − a [ i ] [ n o w ] ) + f [ i − 1 ] [ j ] [ k − 1 ] ∗ a [ i ] [ n o w ] ) 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]) f[i][j][k]=(f[i1][j][k]+f[i1][j1][k](sum[i]a[i][now])+f[i1][j][k1]a[i][now])
其中now表示当前已知食材。
于是就有了 O ( m n 3 ) O(mn^3) O(mn3)的84分优秀做法。
注意到最终的状态只有极小部分的是需要用到了,那么是不是可以考虑一下压缩状态呢?
我们会发现用当前已知食材的菜超过一半,会发现这样就会比其它所有菜的数量加起来要多。
所以我们可以把dp的后两维压成一维,表示当前已知食材做的菜的数量和其它食材做的菜的差,于是就有了dp方程:
f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − 1 ] ∗ a [ i ] [ n o w ] + f [ i − 1 ] [ j + 1 ] ∗ ( s u m [ i ] − a [ i ] [ n o w ] ) 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]) f[i][j]=f[i1][j]+f[i1][j1]a[i][now]+f[i1][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
这道题有个的结论:最优解的最后一段会尽量小
证明的话可以用数学归纳法:
d p [ m ] dp[m] dp[m]表示前m项的答案, s u m [ m ] sum[m] sum[m]表示前m项的和,最后一段最小时左端点为 j j j,最后一段存在的左端点为 k k k
所以问题转化为证明不等式:
d p [ i ] = d p [ j ] + ( s u m [ i ] − s u m [ j ] ) 2 ≤ d p [ k ] + ( s u m [ i ] − s u m [ k ] ) 2 dp[i]=dp[j]+(sum[i]-sum[j])^2\le dp[k]+(sum[i]-sum[k])^2 dp[i]=dp[j]+(sum[i]sum[j])2dp[k]+(sum[i]sum[k])2
i=0就不说了
记当前所求的是前i项的答案
假设 m m m m m m< i i i时原命题成立,
考虑作差法证明不等式:
d p [ j ] + ( s u m [ i ] − s u m [ j ] ) 2 − d p [ k ] − ( s u m [ i ] − s u m [ k ] ) 2 dp[j]+(sum[i]-sum[j])^2-dp[k]-(sum[i]-sum[k])^2 dp[j]+(sum[i]sum[j])2dp[k](sum[i]sum[k])2
= d p [ j ] − d p [ k ] + ( 2 s u m [ i ] − s u m [ k ] − s u m [ j ] ) ( s u m [ k ] − s u m [ j ] ) =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])
≤ d p [ j ] − d p [ k ] − ( s u m [ j ] − s u m [ k ] ) 2 \le dp[j]-dp[k]-(sum[j]-sum[k])^2 dp[j]dp[k](sum[j]sum[k])2
∵ d p [ j ] ≤ d p [ k ] + ( s u m [ j ] − s u m [ k ] ) 2 \because dp[j]\le dp[k]+(sum[j]-sum[k])^2 dp[j]dp[k]+(sum[j]sum[k])2
∴ d p [ j ] − d p [ k ] − ( s u m [ j ] − s u m [ k ] ) 2 ≤ 0 \therefore dp[j]-dp[k]-(sum[j]-sum[k])^2\le0 dp[j]dp[k](sum[j]sum[k])20
∴ d p [ j ] + ( s u m [ i ] − s u m [ j ] ) 2 − d p [ k ] − ( s u m [ i ] − s u m [ k ] ) 2 ≤ 0 \therefore dp[j]+(sum[i]-sum[j])^2-dp[k]-(sum[i]-sum[k])^2\le0 dp[j]+(sum[i]sum[j])2dp[k](sum[i]sum[k])20
原命题得证。
有了这个结论就可以直接一个单调队列求出最后一段的左端点,然后直接算答案就好了。
代码:

#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;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值