ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Codeforces Global Round 14 题解

2021-05-03 22:34:38  阅读:196  来源: 互联网

标签:点亮 int 题解 Global Codeforces 灯泡 连续 序列 dp


E. Phoenix and Computers

题目描述

\(\tt zxy\) 点亮长度为 \(n\) 的序列,如果一个位置两边都被点亮那么这个位置自动点亮,\(\tt zxy\) 不能再次点亮一个已经亮的点,问有多少个不同的操作序列(也就是 \(\tt zxy\) 手动点亮的灯泡或者顺序不同),答案模质数 \(m\)

\(3\leq n\leq 400,10^8\leq m\leq 10^9\)

前言

不知道有没有人和我一样定义状态的,设 \(dp[i]...\) 表示处理前 \(i\) 个位置的方案数 \(...\) 然后发现怎么设计状态都做不动,可能这种做法就是不行吧。

解法1

既然本题又有手动点亮的灯泡,又有自动亮的灯泡,有点麻烦。不妨考虑简化的问题,如果一个长度为 \(x\) 的序列全部灯泡都由手动点亮有多少种方案数?可以枚举第一个点亮的灯泡 \(i\),不难发现 \((i,x]\) 这些灯泡只能按从小到大顺序点亮,\([1,i)\) 这些灯泡也只能按从大到小顺序点亮,两个序列都具有内部顺序,要求合并到一起的方案数。发现就是在 \(x-1\) 的数组中先填 \(i-1\) 个数:

\[\sum_{i=1}^x{x-1\choose i-1}=2^{x-1} \]

这个子问题的结论能不能帮助我们呢?考虑最终的答案实际上是若干个手动点亮的序列拼接起来,然后中间是自动点亮的序列。若干个手动点亮的序列的拼合相当于重新标号的问题,所以可以用 \(\tt EGF\) 卷积来实现,设 \(F(x)\) 为手动点亮方案数的 \(\tt EGF\):

\[F(x)=\sum_{i=1}^{\infty}\frac{2^{i-1}}{i!}x^i \]

那么枚举手动点亮的序列个数 \(i\),可以知道自动点亮的灯泡个数为 \(i-1\),那么手动点亮的灯泡数为 \(n-i+1\),所以可以写出答案:

\[\sum_{i=1}^{(n+1)/2}(n-i+1)![x^{n-i+1}]F^i(x) \]

暴力实现卷积,时间复杂度 \(O(n^3)\),可以用任意模数 \(\tt NTT\) 优化到 \(O(n^2\log n)\)

#include <cstdio>
const int M = 405;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,a[M],b[M],c[M],fac[M],inv[M];
void init()
{
	a[1]=fac[0]=inv[0]=inv[1]=1;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%m;
	for(int i=2;i<=n;i++) inv[i]=inv[m%i]*(m-m/i)%m;
	for(int i=2;i<=n;i++) inv[i]=inv[i]*inv[i-1]%m;
	for(int i=2;i<=n;i++) a[i]=a[i-1]*2%m;
	for(int i=1;i<=n;i++) a[i]=a[i]*inv[i]%m;
}
void mul()
{
	for(int i=0;i<=n;i++)
		for(int j=0;j<=n;j++)
			if(i+j<=n) c[i+j]=(c[i+j]+a[i]*b[j])%m;
	for(int i=0;i<=n;i++)
		b[i]=c[i],c[i]=0;
}
signed main()
{
	n=read();m=read();
	init();b[0]=1;
	for(int i=1;i<=(n+1)/2;i++)
	{
		mul();
		ans=(ans+fac[n-i+1]*b[n-i+1])%m;
	}
	printf("%lld\n",ans);
}

解法2(by jzm)

这个方法其实更重要,因为很多题都用得到他,听说过很久很久了,但是不会

考虑连续段 \(dp\),设 \(dp[i][j]\) 表示 \(j\) 段长度总和是 \(i\) 的方案数,每两段之间任意长(但是长度\(\geq 2\))可以写出转移:

  • 连接两个连续段,这两个连续段长度为 \(2\),有 \(j-1\) 组,每组两种选择:\(dp[i+2][j-1]\leftarrow dp[i][j]\times(j-1)\times 2\)

  • 连接两个连续段,这两个连续段长度为 \(3\),有 \(j-1\) 组,每组一种选择:\(dp[i+3][j-1]\leftarrow dp[i][j]\times(j-1)\)

  • 在某个连续段边上接一个数,连续段个数不变:\(dp[i+1][j]\leftarrow dp[i][j]\times 2j\)

  • 在某个连续段边上隔一个位置接一个数,连续段个数不变:\(dp[i+2][j]\leftarrow dp[i][j]\times 2j\)

  • 新开一个连续段,可以插入到任意两个连续段中:\(dp[i+1][j+1]\leftarrow dp[i][j]\times(j+1)\)

最后答案显然是 \(dp[n][1]\),时间复杂度 \(O(n^2)\)

趁着这个机会好好讲一下连续段 \(dp\),关键的问题是为什么我们能认为两个连续段之间是任意长的?

连续段 \(dp\) 的本质其实是逆向思维,之所以我们这么说是因为我们连续段之间是独立的,而对于连续段的合并其实就相当于原序列的拆分,这个拆分生成了互不相关的子问题,只是一般的 \(dp\) 是从原序列到子问题,连续段 \(dp\) 却是从子问题到原序列,正向是难以进行的,但是反过来是很好做的。

那么连续段 \(dp\) 能解决哪些问题呢?如果是涉及到关于一段连续区间的限制的序列计数问题就可以考虑用它了!

代码是嫖的

#include <bits/stdc++.h>
using namespace std;
typedef long long int_;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
inline void writeint(int x){
	if(x > 9) writeint(x/10);
	putchar((x-x/10*10)^48);
}
int M; // module
inline void add(int&x,const int &y){
	((x += y) >= M) ? (x -= M) : 0;
}
const int MaxN = 404;
int dp[MaxN][MaxN]; // total length, cnt
int main(){
	int n = readint(); M = readint();
	dp[1][1] = 1;
	rep(i,1,n) rep(j,1,i){
		add(dp[i+2][j-1],dp[i][j]*2ll*(j-1)%M); // j-1 gap, 2 choice
		add(dp[i+3][j-1],dp[i][j]*(j-1ll)%M); // j-1 gap, 1 choice
		add(dp[i+1][j],dp[i][j]*2ll*j%M); // 2*j endpos
		add(dp[i+2][j],dp[i][j]*2ll*j%M); // one step from 2*j endpos
		add(dp[i+1][j+1],dp[i][j]*(j+1ll)%M); // j+1 gap
	}
	printf("%d\n",dp[n][1]);
	return 0;
}

F. Phoenix and Earthquake

题目描述

有 \(n\) 个点 \(m\) 条边的无向图,保证图联通,初始时所有的边都被破坏了,如果要重新修建两点之间的边需要 \(x\) 的花费,需要满足 \(a_u+a_v\geq x\) 才能修建边 \((u,v)\),每个城市初始有 \(a_i\) 元钱,如果两个城市联通可以实现资金共享,试构造方案使得图联通,如果不存在方案输出 NO

\(2\leq n,m\leq 300000\)

解法

贪心地看可以选取钱最多的连通块扩展它,你可能会觉得有点小问题,但他是对的。

首先考虑有无解的问题,如果所有城市的资金总和大于等于 \((n-1)\cdot x\) 那么就有解,否则无解。考虑每次选取钱最多的连通块,那么他和任意一个相连的城市都可以连边,因为如果不能的话剩下点资金的总和就大于 \((n-2)\cdot x\),但由于他们单个点的资金数一定小于 \(x\),所以导出矛盾,那么说明在任意时刻都是可以连边的。

可以直接选钱最多的连通块去模拟,用启发式合并可以维护每个点所连的边,时间复杂度 \(O(n\log n)\)

更优美的方法是直接 \(\tt dfs\),首先考虑叶子 \(v\) 和它的父亲 \(u\),如果 \(a_u+a_v\geq x\) 可以暴力连边,否则 \(a_v<x\),那么可以暂时不管 \(v\),剩下的点一定能构成一个大连通块,那么最后再把他接上去就行了,非叶子也类似,整个过程就是递归 \(-\) 回溯的过程,时间复杂度 \(O(n)\)

#include <cstdio>
const int M = 300005;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,x,fr,bk,res,tot,a[M],f[M],vis[M],ans[M];
struct edge
{
	int v,next;
}e[2*M];
void dfs(int u)
{
	vis[u]=1;
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(vis[v]) continue;
		dfs(v);
		if(a[u]+a[v]>=x)
		{
			a[u]+=a[v]-x;
			ans[++fr]=(i+1)/2;
		}
		else
			ans[--bk]=(i+1)/2;
	}
}
signed main()
{
	n=read();m=read();x=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		res+=a[i];
	}
	if(res<(n-1)*x)
	{
		puts("NO");
		return 0;
	}
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read();
		e[++tot]=edge{v,f[u]},f[u]=tot;
		e[++tot]=edge{u,f[v]},f[v]=tot;
	}
	bk=n;
	dfs(1);
	puts("YES");
	for(int i=1;i<n;i++)
		printf("%d\n",ans[i]);
}

标签:点亮,int,题解,Global,Codeforces,灯泡,连续,序列,dp
来源: https://www.cnblogs.com/C202044zxy/p/14728361.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有