2020牛客暑期多校训练营(第七场)题解

A. Social Distancing

给出一个圆,你要在圆内的整数点上放置 n n n 个人,令 d ( i , j ) d(i,j) d(i,j) 表示第 i i i 个人和第 j j j 个人的距离,你要使得 ∑ i = 1 n − 1 ∑ j = i + 1 n d ( i , j ) 2 \sum_{i=1}^{n-1}\sum_{j=i+1}^n d(i,j)^2 i=1n1j=i+1nd(i,j)2 最大。

尝试了模拟退火,但是随机化算法严重不熟练的我连样例都冲不过去……

考虑 d p dp dp,上面那个东西不是很好维护,拆一下:
∑ i = 1 n − 1 ∑ j = i + 1 n ( x i − x j ) 2 + ( y i − y j ) 2 = 1 2 ∑ i = 1 n ∑ j = 1 n x i 2 + x j 2 + y i 2 + y j 2 − 2 x i x j − 2 y i y j = ( ∑ i = 1 n ( x i 2 + y i 2 ) × n ) − ( ∑ i = 1 n x i ) 2 − ( ∑ i = 1 n y i ) 2 \begin{aligned} &\sum_{i=1}^{n-1}\sum_{j=i+1}^n(x_i-x_j)^2+(y_i-y_j)^2\\ &=\frac 1 2\sum_{i=1}^n\sum_{j=1}^n x_i^2+x_j^2+y_i^2+y_j^2-2x_ix_j-2y_iy_j\\ &=\left(\sum_{i=1}^n(x_i^2+y_i^2)\times n\right)-\left(\sum_{i=1}^n x_i\right)^2-\left(\sum_{i=1}^n y_i\right)^2 \end{aligned} i=1n1j=i+1n(xixj)2+(yiyj)2=21i=1nj=1nxi2+xj2+yi2+yj22xixj2yiyj=(i=1n(xi2+yi2)×n)(i=1nxi)2(i=1nyi)2

于是可以定义 d p dp dp 状态: f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k] 表示 i i i 个人, x x x 坐标之和为 j j j y y y 坐标之和为 k k k,上面式子中第一项的最大值。

求出这个后,再求 a n s [ i ] [ r ] ans[i][r] ans[i][r] 表示 i i i 个人,半径为 r r r 的最大答案,用 f [ i ] [ j ] [ k ] − j 2 − k 2 f[i][j][k]-j^2-k^2 f[i][j][k]j2k2 来更新即可。

代码如下:

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
#define off 300

int T,n,m;
struct point{
    
    int x,y,dis;};
vector<point> s;
bool cmp(point x,point y){
    
    return x.dis>y.dis;}
int f[9][610][610],ans[9][31];

int main()
{
    
    
	for(int i=-30;i<=30;i++){
    
    
		for(int j=-30;j<=30;j++){
    
    
			s.push_back((point){
    
    i,j,i*i+j*j});
		}
	}
	for(int i=0;i<=8;i++){
    
    
		for(int x=0;x<=2*off;x++){
    
    
			for(int y=0;y<=2*off;y++)
			f[i][x][y]=-2e9;
		}
	}
	f[0][off][off]=0;
	sort(s.begin(),s.end(),cmp);
	for(int r=1;r<=30;r++){
    
    
		while(s.back().dis<=r*r){
    
    
			point p=s.back();s.pop_back();
			for(int i=1;i<=8;i++){
    
    
				for(int x=-i*r+off;x<=i*r+off;x++){
    
    
					for(int y=-i*r+off;y<=i*r+off;y++){
    
    
						f[i][x][y]=max(f[i][x][y],f[i-1][x-p.x][y-p.y]+p.dis);
					}
				}
			}
		}
		for(int i=1;i<=8;i++){
    
    
			for(int x=0;x<=2*off;x++){
    
    
				for(int y=0;y<=2*off;y++)if(f[i][x][y]>0)
				ans[i][r]=max(ans[i][r],i*f[i][x][y]-(x-off)*(x-off)-(y-off)*(y-off));
			}
		}
	}
	scanf("%d",&T);while(T--)scanf("%d %d",&n,&m),printf("%d\n",ans[n][m]);
}

B. Mast Allocation

n × m n\times m n×m 个口罩分成 k k k 份,使得可以用这 k k k 份凑出 n n n 组,每组 m m m 个口罩,也可以凑成 m m m 组每组 n n n 个口罩,找出 k k k 最小的字典序最大的方案。

n > m n>m n>m,由于可以凑出 n n n × \times × m m m 个,所以每份大小不能超过 m m m,而又能凑出 m m m × \times × n n n 个,因为要让 k k k 最小,所以让每份尽可能大,所以这里每组内可以放 ⌊ n m ⌋ \lfloor \frac n m \rfloor mn 份大小为 m m m 的,那么就有 ⌊ n m ⌋ × m \lfloor \frac n m \rfloor \times m mn×m 份确定下来了。

此时 n n n × \times × m m m 个已经有了 ⌊ n m ⌋ × m \lfloor \frac n m \rfloor \times m mn×m 组确定了,还有 n   m o d   m n\bmod m nmodm 组没放, m m m × \times × n n n 个每组内还剩 n   m o d   m n\bmod m nmodm 个要放。那么此时就变成了规模为 m , n   m o d   m m,n\bmod m m,nmodm 的与上面问题相同的问题,递归下去就做完了。

代码如下:

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
#define pb push_back

int T,n,m;
vector<int>ans;
void solve(int x,int y){
    
    
	if(x<y)swap(x,y);
	if(!y)return;
	int p=x/y*y;
	while(p--)ans.pb(y);
	solve(x%y,y);
}

int main()
{
    
    
	scanf("%d",&T);while(T--)
	{
    
    
		scanf("%d %d",&n,&m);
		ans.clear();
		solve(n,m);
		printf("%d\n",(int)ans.size());
		for(int i:ans)printf("%d ",i);printf("\n");
	}
}

C. A National Pandemic

给出一棵树,有三种操作:1、给出 x , w x,w x,w,使每个点 y y y F ( y ) F(y) F(y) 加上 w − d i s ( x , y ) w-dis(x,y) wdis(x,y);2、给出 x x x,令 F ( x ) = min ⁡ ( 0 , F ( x ) ) F(x)=\min(0,F(x)) F(x)=min(0,F(x));3、给出 x x x,询问 F ( x ) F(x) F(x)

令每个点的 F ( x ) F(x) F(x) 与父亲的 F ( f a ) F(fa) F(fa) 做差分,记为 b ( x ) b(x) b(x),设 1 1 1 为根节点,那么 1 1 1 操作相当于让 1 1 1 ~ x x x 路径上的 b ( x ) + 1 b(x)+1 b(x)+1(不包括 1 1 1),然后让 1 1 1 加上 w − d i s ( 1 , x ) w-dis(1,x) wdis(1,x),然后其他点的 b ( x ) − 1 b(x)-1 b(x)1

那么询问一个点的 F ( x ) F(x) F(x) 时,求一下根节点到它路径上所有点的 b ( x ) b(x) b(x) 之和即可。

对于二操作,如果直接修改 F ( x ) F(x) F(x),那么会影响到儿子的 b ( x ) b(x) b(x),可以发现这个操作只会影响到一个点,于是考虑记录一个 s [ x ] s[x] s[x],每次用 s [ x ] s[x] s[x] 记录下这个变化量即可。

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 100010
#define int long long

int T,n,m;
struct edge{
    
    int y,next;}e[maxn<<1];
int first[maxn],len=0;
void buildroad(int x,int y){
    
    e[++len]=(edge){
    
    y,first[x]};first[x]=len;}
int dep[maxn],fa[maxn],size[maxn],mson[maxn];
void dfs1(int x){
    
    
	size[x]=1;mson[x]=0;
	for(int i=first[x];i;i=e[i].next){
    
    
		int y=e[i].y;if(y==fa[x])continue;
		fa[y]=x;dep[y]=dep[x]+1;dfs1(y);
		size[x]+=size[y];
		if(size[y]>size[mson[x]])mson[x]=y;
	}
}
int top[maxn],id[maxn],idtot;
void dfs2(int x,int tp){
    
    
	top[x]=tp;id[x]=++idtot;
	if(mson[x])dfs2(mson[x],tp);
	for(int i=first[x];i;i=e[i].next)
	if(e[i].y!=fa[x]&&e[i].y!=mson[x])dfs2(e[i].y,e[i].y);
}
#define zuo ch[0]
#define you ch[1]
struct node{
    
    
	int l,r,mid,z,lazy;node *ch[2];
	node(int x,int y):l(x),r(y),mid(l+r>>1),z(0),lazy(0){
    
    
		if(x<y){
    
    
			zuo=new node(l,mid);
			you=new node(mid+1,r);
		}else zuo=you=NULL;
	}
	void update(int x){
    
    z+=(r-l+1)*x;lazy+=x;}
	void pushdown(){
    
    if(zuo)zuo->update(lazy),you->update(lazy);lazy=0;}
	void change(int x,int y,int c){
    
    
		pushdown();
		if(l==x&&r==y)return update(c);
		if(y<=mid)zuo->change(x,y,c);
		else if(x>=mid+1)you->change(x,y,c);
		else zuo->change(x,mid,c),you->change(mid+1,y,c);
		z=zuo->z+you->z;
	}
	int ask(int x,int y){
    
    
		pushdown();
		if(l==x&&r==y)return z;
		if(y<=mid)return zuo->ask(x,y);
		else if(x>=mid+1)return you->ask(x,y);
		else return zuo->ask(x,mid)+you->ask(mid+1,y);
	}
}*root=NULL;
int go(int x,int type){
    
    //0ÊÇÐÞ¸Ä,1ÊÇÇóºÍ 
	int re=0;
	while(top[x]!=1){
    
    
		if(type)re+=root->ask(id[top[x]],id[x]);
		else root->change(id[top[x]],id[x],2);
		x=fa[top[x]];
	}
	if(type)re+=root->ask(id[1],id[x]);
	else if(x!=1)root->change(id[1]+1,id[x],2);
	return re;
}
int s[maxn];

signed main()
{
    
    
	scanf("%lld",&T);while(T--)
	{
    
    
		scanf("%lld %lld",&n,&m);
		memset(first,0,sizeof(first));len=0;
		memset(s,0,sizeof(s));
		for(int i=1,x,y;i<n;i++){
    
    
			scanf("%lld %lld",&x,&y);
			buildroad(x,y),buildroad(y,x);
		}
		dfs1(1);idtot=0;dfs2(1,1);
		root=new node(1,n);
		for(int i=1,op,x,w;i<=m;i++){
    
    
			scanf("%lld %lld",&op,&x);
			if(op==1){
    
    
				scanf("%lld",&w);
				root->change(1,n,-1);
				go(x,0);
				root->change(id[1],id[1],w-dep[x]+1);
			}else if(op==2){
    
    
				int p=go(x,1)+s[x];
				if(p>0)s[x]-=p;
			}else printf("%lld\n",go(x,1)+s[x]);
//			for(int j=1;j<=n;j++)printf("%lld ",go(j,1)+s[j]);printf("\n");
		}
	}
}
/*
1
9 4
1 2
2 3
2 4
2 5
1 6
6 7
7 8
7 9
1 1 2
1 8 3
2 3
2 1
*/

D. Fake News

T T T 组询问,每次问 ∑ i = 1 n i 2 \sum_{i=1}^n i^2 i=1ni2 是不是平方数。

题面好评

有一个广为人知的柿子: ∑ i = 1 n i 2 = n ( n + 1 ) ( 2 n + 1 ) / 6 \sum_{i=1}^n i^2=n(n+1)(2n+1)/6 i=1ni2=n(n+1)(2n+1)/6

也就是说要满足 n ( n + 1 ) ( 2 n + 1 ) / 6 n(n+1)(2n+1)/6 n(n+1)(2n+1)/6 是平方数,除法不好解决,先乘上个 36 36 36 变成 6 n ( n + 1 ) ( 2 n + 1 ) 6n(n+1)(2n+1) 6n(n+1)(2n+1)

首先可以发现 n n n n + 1 , 2 n + 1 n+1,2n+1 n+1,2n+1 互质。

n n n 为奇数时, n n n 6 6 6 互质,此时 n n n 必须是完全平方数,而 2 n + 1 2n+1 2n+1 与剩余两者互质,也必须是完全平方数,然后再满足 6 ( n + 1 ) 6(n+1) 6(n+1) 也是完全平方数即可。

n n n 为偶数时, 6 6 6 n + 1 n+1 n+1 互质,类似地判一下就好了。

代码如下:

#include <cstdio>
#include <cmath>
#define ll long long

int T;ll n;
bool pf(ll x){
    
    ll p=sqrt(x);return p*p==x;}

int main()
{
    
    
	scanf("%d",&T);while(T--)
	{
    
    
		scanf("%lld",&n);
		bool v=(n==1);
		if(n%2)v|=(pf(6*(n+1))&&pf(n)&&pf(2*n+1));
		else v|=(pf(6*n)&&pf(n+1)&&pf(2*n+1));
		if(v)printf("Fake news!\n");
		else printf("Nobody knows it better than me!\n");
	}
}

翻了下题解发现其实只需要判 1 1 1 24 24 24 就好了,证明不是很懂……

F. Tokens on the Tree

给出一棵树,树上有些点被染了黑或白色,同色的点必定都相邻,对于一个染色点 x x x,他可以通过路径 p 1 , p 2 , . . . , p k p_1,p_2,...,p_k p1,p2,...,pk 将颜色给点 p k p_k pk,其中 p 1 = x p_1=x p1=x p 1 p_1 p1 ~ p k − 1 p_{k-1} pk1 颜色相同, p k p_k pk 无颜色。假如 A A A 树可以通过进行这个传递方式变成 B B B 树,那么称这两棵树等价,令 f ( w , b ) f(w,b) f(w,b) 表示白点有 w w w 个,黑点有 b b b 个时等价类数量,求 ∑ w = 1 n − 1 ∑ b = 1 n − w w × b × f ( w , b ) \sum_{w=1}^{n-1}\sum_{b=1}^{n-w}w\times b\times f(w,b) w=1n1b=1nww×b×f(w,b)

w > b w>b w>b。考虑固定一个 w w w,先求出必定是白点的点,去掉这些点后树被分成了若干个联通块,对于一个 b b b f ( w , b ) f(w,b) f(w,b) 就是 节点数 ≥ b \geq b b 的连通块数量。

考虑一个点什么时候必定是白点:注意到同色点必定是连通的,那么当这个点的某棵子树大小 ≥ w \geq w w 时,就存在一种方案将所有白点放在这棵子树内,此时这个点不是白点。

也就是说,假如一个点的所有子树大小都小于 w w w,那么这个点必定是白点。于是可以预处理出每个点的重子树大小,然后排个序,从大到小枚举 w w w,每次将重子树大小 ≥ w \geq w w 的点变成非必定白点,用并查集维护非必定白点的连通块大小,令 s ( b ) s(b) s(b) 表示大小 ≥ b \geq b b 的联通块数量,用线段树维护 s ( b ) × b s(b)\times b s(b)×b 就可以求解了。

再考虑不存在必定是白点的点的情况,此时对于一组 w , b w,b w,b,假如存在一个点,他有两棵子树大小 ≥ w \geq w w,有另外一棵子树大小 ≥ b \geq b b,那么白色块和黑色块此时是可以到达任意位置的,即 f ( w , b ) = 1 f(w,b)=1 f(w,b)=1,否则 f ( w , b ) = 2 f(w,b)=2 f(w,b)=2,这个简单思考一下就能明白。

于是考虑求出每个点的第二第三大的子树大小,按第二大排序,在上面的基础上继续枚举 w w w,求出第二大子树 ≥ w \geq w w 的点中,第三大的最大值 m a ma ma,那么当 1 ≤ b ≤ m a 1\leq b\leq ma 1bma 时, b b b 的贡献就是 1 1 1 m a < b ≤ w ma<b\leq w ma<bw 时,贡献就是 2 2 2

还要注意统计的时候有一个细节,对于 b < w b<w b<w 的贡献,我们最后是要乘二的,因为还要算上 b > w b>w b>w 的贡献,而 b = w b=w b=w 的不需要,所以需要分别记录。

代码如下:

#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 400010
#define mod 1000000007ll
#define pb push_back

int T,n,ans1,ans2;
vector<int> e[maxn];
int size[maxn],mson[maxn],rt;
struct par{
    
    int x,sz;par(int X=0,int Y=0):x(X),sz(Y){
    
    }};
vector<par> c;
bool cmp1(par x,par y){
    
    return x.sz>y.sz;}
void dfs1(int x,int fa,int type){
    
    
	size[x]=1;mson[x]=0;
	for(int y:e[x])if(y!=fa){
    
    
		dfs1(y,x,type);size[x]+=size[y];
		if(size[y]>mson[x])mson[x]=size[y];
	}
	if(n-size[x]>mson[x])mson[x]=n-size[x];
	if(!type){
    
    
		if(mson[x]<mson[rt])rt=x;
	}else{
    
    
		c.pb(par(x,mson[x]));
	}
}
void add(int &x,int y){
    
    x=(x+y>=mod?x+y-mod:x+y);}
int add(int x){
    
    return x>=mod?x-mod:x;}
struct node{
    
    
	int l,r,mid,val,sum,lazy;node *zuo,*you;
	node(int x,int y):l(x),r(y),mid(l+r>>1),sum(0),lazy(0){
    
    
		if(x<y){
    
    
			zuo=new node(x,mid);you=new node(mid+1,y);
			val=add(zuo->val+you->val);
		}else zuo=you=NULL,val=l;
	}
	void update(int x){
    
    lazy+=x;sum=(1ll*sum+val*x%mod+mod)%mod;}
	void pushdown(){
    
    if(zuo)zuo->update(lazy),you->update(lazy);lazy=0;}
	void change(int x,int y,int z){
    
    
		pushdown();
		if(l==x&&r==y)return update(z);
		if(y<=mid)zuo->change(x,y,z);
		else if(x>=mid+1)you->change(x,y,z);
		else zuo->change(x,mid,z),you->change(mid+1,y,z);
		sum=add(zuo->sum+you->sum);
	}
	int getsum(int x,int y){
    
    
		pushdown();
		if(l==x&&r==y)return sum;
		if(y<=mid)return zuo->getsum(x,y);
		else if(x>=mid+1)return you->getsum(x,y);
		else return add(zuo->getsum(x,mid)+you->getsum(mid+1,y));
	}
}*root;
int fa[maxn],v[maxn];
int findfa(int x){
    
    return x==fa[x]?x:fa[x]=findfa(fa[x]);}
vector<int> subs[maxn];
void dfs2(int x,int Fa){
    
    
	size[x]=1;
	for(int y:e[x])if(y!=Fa){
    
    
		dfs2(y,x);size[x]+=size[y];
		subs[x].pb(size[y]);
	}
	if(size[x]<n)subs[x].pb(n-size[x]);
}
bool cmp2(int x,int y){
    
    return x>y;}
int kk(int x,int y){
    
    return (1ll*(x+y)*(y-x+1)/2ll)%mod;}

int main()
{
    
    
	scanf("%d",&T);while(T--)
	{
    
    
		scanf("%d",&n);ans1=0;ans2=0;
		for(int i=1;i<=n;i++)e[i].clear();
		for(int i=2,x;i<=n;i++)scanf("%d",&x),e[x].pb(i),e[i].pb(x);
		mson[rt=0]=1e9;dfs1(1,0,0);c.clear();dfs1(rt,0,1);
		sort(c.begin(),c.end(),cmp1);root=new node(1,n);
		for(int i=1;i<=n;i++)fa[i]=i,size[i]=1,v[i]=0;
		int w=n-1,st=0;for(;w>0;w--){
    
    
			while(st<c.size()&&c[st].sz>=w){
    
    
				int x=c[st++].x;v[x]=1;
				for(int y:e[x])if(v[y]){
    
    //与周围的非必白点合并成一块
					int p=findfa(y);
					fa[p]=x;size[x]+=size[p];
					root->change(1,size[p],-1);
				}
				root->change(1,size[x],1);
			}
			if(st==c.size())break;//必白点点集为空
			int mi=min(w,n-w); if(mi<w)add(ans1,1ll*w*root->getsum(1,mi)%mod);
			else add(ans1,1ll*w*root->getsum(1,mi-1)%mod),add(ans2,1ll*w*root->getsum(mi,mi)%mod);
		}
		for(int i=1;i<=n;i++)subs[i].clear();dfs2(rt,0);
		c.clear();for(int i=1;i<=n;i++)if(subs[i].size()>2){
    
    
			sort(subs[i].begin(),subs[i].end(),cmp2);
			c.pb(par(subs[i][2],subs[i][1]));
		}
		sort(c.begin(),c.end(),cmp1);
		st=0;int ma=0;for(;w>0;w--){
    
    
			while(st<c.size()&&c[st].sz>=w)ma=max(ma,c[st].x),st++;
			int mi=min(w,n-w),la;
			if(ma>=mi)add(ans1, 1ll*w*kk(1,mi)%mod ),la=1ll*w*mi%mod;
			else add(ans1,add( 1ll*w*kk(1,ma)%mod + 2ll*w*kk(ma+1,mi)%mod )),la=2ll*w*mi%mod;
			if(mi==w)ans1=(ans1-la+mod)%mod,add(ans2,la);
		}
		printf("%lld\n",(2ll*ans1+ans2)%mod);
	}
}

G. Topo Counting

给出一张晒肉架图, V 1 V^1 V1 为架子,剩下的称为肉片。求其拓扑序列数量。

有一个性质,对于两个不相关的有向图,令他们大小分别为 s 1 , s 2 s_1,s_2 s1,s2,拓扑序列数分别为 t 1 , t 2 t_1,t_2 t1,t2,那么总拓扑序列数就是 C s 1 + s 2 s 1 t 1 t 2 C_{s_1+s_2}^{s_1}t_1t_2 Cs1+s2s1t1t2

再看晒肉架图,考虑与架子相连的图,大致有三种形态:
在这里插入图片描述
第一种是只拿走了肉架第一行第一个,这张图只与第一行长度相关,记为 h ( i ) h(i) h(i)

第二种是拿走了肉架第一行的若干个,与第一行和第二行数量相关,记为 f ( i , j ) f(i,j) f(i,j)

第三种是拿走了肉架两行的第一个,并拿走了第一个肉片的左边部分若干个,这种与肉架长度、第一个肉片的左边部分剩余节点数相关,记为 g ( i , j ) g(i,j) g(i,j)

转移的话,先考虑肉片的方案数:这个简单 dp \text{dp} dp 一下就行,令 w ( i , j ) w(i,j) w(i,j) 表示肉片左边剩 i i i 个,右边剩 j j j 个的拓扑序列数,只有两个或一个节点是可以拿的,所以很好转移。

容易发现, h ( i ) h(i) h(i) 只能转移到 f ( i − 1 , i + 1 ) f(i-1,i+1) f(i1,i+1) g ( i , n ) g(i,n) g(i,n)

f ( i , j ) f(i,j) f(i,j) 只能转移到 f ( i − 1 , m ) f(i-1,m) f(i1,m) 或 ( f ( i , j − 1 ) , h ( i ) f(i,j-1),h(i) f(i,j1),h(i) 其中一者加一块完整肉片)。

g ( i , j ) g(i,j) g(i,j) 只能转移到 g ( i , j − 1 ) g(i,j-1) g(i,j1) h ( i − 1 ) h(i-1) h(i1) 加肉片剩余部分。

于是就做完了,代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 3010

int n,mod;
int fac[maxn*maxn<<1],inv_fac[maxn*maxn<<1];
void work(){
    
    int N=n*n*2;
	fac[0]=inv_fac[0]=inv_fac[1]=1;
	for(int i=1;i<=N;i++)fac[i]=1ll*fac[i-1]*i%mod;
	for(int i=2;i<=N;i++)inv_fac[i]=1ll*(mod-mod/i)*inv_fac[mod%i]%mod;
	for(int i=1;i<=N;i++)inv_fac[i]=1ll*inv_fac[i]*inv_fac[i-1]%mod;
}
int C(int x,int y){
    
    return 1ll*fac[x]*inv_fac[y]%mod*inv_fac[x-y]%mod;}
void add(int &x,int y){
    
    x=(x+y>=mod?x+y-mod:x+y);}
int add(int x){
    
    return x>=mod?x-mod:x;}
int w[maxn][maxn],g[maxn][maxn],h[maxn],f[maxn][maxn];
void dp(){
    
    
	for(int i=0;i<=n;i++){
    
    
		for(int j=i;j<=n;j++){
    
    
			if(i==0){
    
    w[i][j]=1;continue;}
			w[i][j]=w[i-1][j];
			if(j>i)add(w[i][j],w[i][j-1]);
		}
	}
	for(int i=0;i<n;i++){
    
    
		for(int j=0;j<=n;j++){
    
    
			if(i==0)break;
			if(j>0)g[i][j]=g[i][j-1];
			int cnt=2*i-1+(i-1)*(2*n);
			add(g[i][j],1ll*w[j][n]*h[i-1]%mod*C(cnt+j+n,j+n)%mod);
		}
		if(i==0)h[i]=1;
		else h[i]=add(f[i-1][i+1]+g[i][n]);
		for(int j=i+2,coe;j<=n;j++){
    
    
			if(i>0)f[i][j]=f[i-1][j];
			if(j==i+2)coe=h[i];else coe=f[i][j-1];
			int cnt=i+(j-1)*(2*n+1)-2*n;
			add(f[i][j],1ll*w[n][n]*coe%mod*C(cnt+2*n,2*n)%mod);
		}
	}
}

int main()
{
    
    
	scanf("%d %d",&n,&mod);
	work();dp();
	printf("%d",h[n-1]);
}

H. Dividing

定义传奇二元组: ( 1 , k ) (1,k) (1,k) 一定是传奇二元组,如果 ( n , k ) (n,k) (n,k) 是,那么 ( n + k , k ) (n+k,k) (n+k,k) ( n k , k ) (nk,k) (nk,k) 也一定是,问有多少个传奇二元组 ( n , k ) (n,k) (n,k) 满足 1 ≤ n ≤ N , 1 ≤ k ≤ K 1\leq n \leq N,1\leq k \leq K 1nN,1kK

k k k 固定时,最小的传奇二元组为 ( 1 , k ) (1,k) (1,k) ( k , k ) (k,k) (k,k),容易发现,剩下的传奇二元组都可以表示成 ( 1 + d k , k ) (1+dk,k) (1+dk,k) ( d k , k ) (dk,k) (dk,k),数量为 ⌊ N k ⌋ + ⌊ N − 1 k ⌋ + 1 \lfloor \frac N k \rfloor + \lfloor \frac {N-1} k \rfloor+1 kN+kN1+1

于是可以考虑除法分块,注意要特判 k = 1 k=1 k=1 的情况。

代码如下:

#include <cstdio>
#define ll long long
#define mod 1000000007

ll n,m,ans;
void solve(){
    
    
	for(ll l=2,r;l<=n;l=r+1){
    
    
		r=n/(n/l);
		ans=(ans+(r-l+1)%mod*(n/l)%mod)%mod;
	}
}

int main()
{
    
    
	scanf("%lld %lld",&n,&m);
	ans=n+m-1;
	solve();n--;solve();
	printf("%lld",ans%mod);
}

I. Valueable Forests

定义一棵树的贡献为所有点的度数的平方之和,一个森林的贡献为所有树的贡献之和,求所有 n n n 个点的森林的贡献之和。

根据 Cayley \text{Cayley} Cayley 公式,可以知道 n n n 个点的无根树数为 n n − 2 n^{n-2} nn2

f ( n ) f(n) f(n) 表示 n n n 个点的树的贡献之和,那么有:
f ( n ) = ∑ i = 1 n ∑ j = 1 n − 1 j 2 × C n − 2 j − 1 × ( n − 1 ) n − 2 − ( j − 1 ) f(n)=\sum_{i=1}^n\sum_{j=1}^{n-1}j^2\times C_{n-2}^{j-1}\times(n-1)^{n-2-(j-1)} f(n)=i=1nj=1n1j2×Cn2j1×(n1)n2(j1)

i i i 是枚举一个点,后面的式子是求这个点的贡献之和, j j j 枚举它的度, C n − 2 j − 1 C_{n-2}^{j-1} Cn2j1 是在 P r u ¨ f e r Pr\ddot ufer Pru¨fer 编码里给它找位置,剩下的就随便放数字。

发现 i i i 没有用,于是:
f ( n ) = n ∑ j = 1 n − 1 j 2 × C n − 2 j − 1 × ( n − 1 ) n − 2 − ( j − 1 ) f(n)=n\sum_{j=1}^{n-1}j^2\times C_{n-2}^{j-1}\times(n-1)^{n-2-(j-1)} f(n)=nj=1n1j2×Cn2j1×(n1)n2(j1)

再考虑求 d ( n ) d(n) d(n) 表示 n n n 个点的森林数,这个很好办:
d ( n ) = ∑ i = 1 n C n − 1 i − 1 d ( n − i ) × i i − 2 d(n)=\sum_{i=1}^n C_{n-1}^{i-1}d(n-i)\times i^{i-2} d(n)=i=1nCn1i1d(ni)×ii2

i i i 是枚举 1 1 1 所在的树大小, C n − 1 i − 1 C_{n-1}^{i-1} Cn1i1 是找另外 i − 1 i-1 i1 个点和 1 1 1 组成树。

最后设 g ( n ) g(n) g(n) n n n 个点的森林的总贡献,类似 d ( n ) d(n) d(n) 的求法,考虑 1 1 1 所在的树的大小,可以得到:
g ( n ) = ∑ i = 1 n C n − 1 i − 1 ( d ( n − i ) × f ( i ) + i i − 2 × g ( n − i ) ) g(n)=\sum_{i=1}^n C_{n-1}^{i-1}(d(n-i)\times f(i)+i^{i-2}\times g(n-i)) g(n)=i=1nCn1i1(d(ni)×f(i)+ii2×g(ni))

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 5010

int T,mod,n;
int C[maxn][maxn],cay[maxn],f[maxn],d[maxn],g[maxn];
int ksm(int x,int y){
    
    int re=1;for(;(y&1?re=1ll*re*x%mod:0),y;y>>=1,x=1ll*x*x%mod);return re;}
void add(int &x,int y){
    
    x=(x+y>=mod?x+y-mod:x+y);}
int add(int x){
    
    return x>=mod?x-mod:x;}
void work(){
    
    int N=maxn-10;
	C[0][0]=1;
	for(int i=1;i<=N;i++){
    
    
		C[i][0]=1;
		for(int j=1;j<=i;j++)
		C[i][j]=add(C[i-1][j-1]+C[i-1][j]);
	}
	cay[1]=1;for(int i=2;i<=N;i++)cay[i]=ksm(i,i-2);
	for(int i=2;i<=N;i++){
    
    
		for(int j=i-1,prod=1;j>=1;j--,prod=1ll*prod*(i-1)%mod)
		add(f[i],1ll*j*j%mod*C[i-2][j-1]%mod*prod%mod);
		f[i]=1ll*f[i]*i%mod;
	}
	d[0]=1;for(int i=1;i<=N;i++){
    
    
		for(int j=1;j<=i;j++){
    
    
			add(d[i],1ll*C[i-1][j-1]*d[i-j]%mod*cay[j]%mod);
		}
	}
	for(int i=1;i<=N;i++){
    
    
		for(int j=1;j<=i;j++){
    
    
			add(g[i],1ll*C[i-1][j-1]*(1ll*d[i-j]*f[j]%mod+1ll*cay[j]*g[i-j]%mod)%mod);
		}
	}
}

signed main()
{
    
    
	scanf("%d %d",&T,&mod);work();
	while(T--)scanf("%d",&n),printf("%d\n",g[n]);
}

很奇怪,这题用阶乘求组合数会WA,通过率是95%,不知道是蒟蒻写错了还是它m出现了不为质数的情况qwq

J. Pointer Analysis

26 26 26 个对象,每个对象内有 26 26 26 个指针,有另外 26 26 26 个全局指针,给出 n n n 条指令,每次操作执行一条指令,随机进行无数次操作后,问每个全局指针有可能指向哪些对象。

记录每个对象内的每个指针能够指向的对象集合,记为 P [ i ] [ j ] P[i][j] P[i][j],每个全局指针能够指向的对象集合记为 p [ i ] p[i] p[i],将这个指令集执行 26 26 26 次,每次将两个操作的指针集合进行合并即可。因为每个集合大小不超过 26 26 26,所以执行 26 26 26 次后每个集合就一定是最终答案。

代码如下:

#include <cstdio>
#include <cstring>
#include <set>
#include <vector>
#include <algorithm>
using namespace std;

int n,len[210];
char s[210][30];
int kk(char x){
    
    
	if(x>='a'&&x<='z')return x-'a'+1;
	if(x>='A'&&x<='Z')return x-'A'+1;
	return 0;
}
set<int> p[30],P[30][30];
void merge(set<int> &from,set<int> &to){
    
    for(int x:from)if(!to.count(x))to.insert(x);}
//A = B
//A = o
//A.f = B
//A = B.f

int main()
{
    
    
	scanf("%d\n",&n);
	for(int i=1;i<=n;i++)gets(s[i]+1),len[i]=strlen(s[i]+1);
	for(int j=1;j<=26;j++){
    
    
		for(int i=1;i<=n;i++){
    
    
			int a=kk(s[i][1]),b=kk(s[i][3]),c=kk(s[i][5]),d=kk(s[i][7]);
			if(len[i]==5){
    
    
				if(s[i][5]>='a'&&s[i][5]<='z')p[a].insert(c);
				else merge(p[c],p[a]);
			}else{
    
    
				if(s[i][5]=='=')for(int k:p[a])merge(p[d],P[k][b]);
				else for(int k:p[c])merge(P[k][d],p[a]);
			}
		}
	}
	for(int i=1;i<=26;i++){
    
    
		printf("%c: ",'A'+i-1);
		for(int j:p[i])printf("%c",'a'+j-1);
		printf("\n");
	}
}

猜你喜欢

转载自blog.csdn.net/a_forever_dream/article/details/108460386
今日推荐