~~课件~~

CF1350B

Solution

很简单的一道 d p dp 题。

状态设计 d p i dp_i 表示从 1 1 号位置看到 i i 号位置的选的数的数量的最大值。容易发现,下标为 i i 的数可以跟在任何一个下标为 j ( j i ) j(j|i) 的数的后面,且必须满足其中 a j a i a_j<a_i

注意答案并不是 d p n dp_n ,而是 d p dp 数组中的最大值。注意将数 i i 的约数的个数的时间复杂度约束在 O ( i ) O(\sqrt{i}) 以内。

时间复杂度为 O ( Σ i = 1 n i ) O(Σ_{i=1}^n \sqrt{i}) ,即 O ( n l o g 2 n ) O(n log_2n)

Code

#include <bits/stdc++.h>
#define int long long
using namespace std;
 
int t,n;
int a[100005],dp[100005];
 
signed main()
{
	cin>>t;
	while (t--)
	{
		cin>>n;
		for (int i=1;i<=n;i++)  cin>>a[i];
		
		int ans=-1;
		for (int i=1;i<=n;i++)
		{
			int k=i,maxv=0;
			for (int j=1;j*j<=k;j++)
			{
				if (i%j==0)
				{
					if (a[j]<a[i])  maxv=max(maxv,dp[j]);
					if (a[i/j]<a[i])  maxv=max(maxv,dp[i/j]);
				}
			}
			dp[i]=maxv+1;
			ans=max(ans,dp[i]);
		}
		cout<<ans<<endl;
	}
	return 0;
}

CF1349A

这不是一道水题,尽管这是某场比赛的 A A 题。

Solution

容易发现,如果正整数 k k 作为了 n n 个数中至少 n 1 n-1 个数的约数,那么答案就一定有 k k 这个因数。

因此,我们只需要枚举质数 k k 加上一定的剪枝就可以了。即,如果已经看到了 2 2 个数并不是 k k 的倍数,那么就直接跳出并宣布答案并不又含 k k 这个因数;同时,如果发现它是 k k 的倍数,就直接将它除以 k k ;这种除法一定不会影响答案的正确性,因为任何两个不同的质数一定互质

注意答案可能含有多个质因子 k k ,所以我们要不停地用 k k 来进行筛查,直到检查失败(即超过两个数无法被 k k 整除)为止。答案就是所有 k k 的积。

扫描二维码关注公众号,回复: 11266998 查看本文章

核心代码如下:

for (int i=1;i<=cnt;i++)
{
	int count=0;
	while (1)
	{
		int pos=0,flag=1;
		for (int j=1;j<=n;j++)
		{
			if (a[j]%b[i]==0)  a[j]/=b[i];
			else pos++;
			
			if (pos==2)
			{
				flag=0;
				break;
			}
		}
		if (flag==1)  count++;
		else break;
	}
	tot=tot*quick_power(b[i],count);
}

Code

#include <bits/stdc++.h>
#define int long long
#define inf 2000000007
using namespace std;
 
int n,tot=1,cnt=0;
int prime[200005],a[200005],b[200005];
 
int quick_power(int x,int y)
{
	int res=1;
	for (;y;y=y>>1,x=(x*x))
	{
		if (y&1)  res=res*x;
	}
	return res;
}
 
signed main()
{
	cin>>n;
	for (int i=1;i<=n;i++)  cin>>a[i];
	for (int i=2;i<=200000;i++)  prime[i]=1;
	for (int i=2;i<=200000;i++)
	{
		if (prime[i]==0)  continue;
		for (int j=2*i;j<=200000;j+=i)  prime[j]=0;
	}
	for (int i=2;i<=200000;i++)
	{
		if (prime[i]==1)  b[++cnt]=i;
	}
	for (int i=1;i<=cnt;i++)
	{
		int count=0;
		while (1)
		{
			int pos=0,flag=1;
			for (int j=1;j<=n;j++)
			{
				if (a[j]%b[i]==0)  a[j]/=b[i];
				else pos++;
				
				if (pos==2)
				{
					flag=0;
					break;
				}
			}
			if (flag==1)  count++;
			else break;
		}
		tot=tot*quick_power(b[i],count);
	}
	cout<<tot<<endl;
	
	return 0;
}

P6512

蛮综合的一道好题!

Solution

Part 1

首先,我们把所有猪猪出现的时间从小到大排序。

考虑: 如果要抓第 i i 只猪猪,那么第 j ( j > i ) j(j>i) 只猪猪能否也被抓到。注意这里眼里只有第i只和第j只猪猪,没有别的猪猪

假定第 i i 只猪猪在 t 1 t1 时刻, x 1 x1 号节点上出现;第 j j 只猪猪在第 t 2 t2 时刻, x 2 x2 号节点上出现 ( j > i ) (j>i) 。如果从 x 1 x1 x 2 x2 的最短耗时不大于 t 2 t2 t 1 t1 的差(时间差),那么它们就可能一起被抓到;否则不可能一起被抓到(因为抓了一只就来不及赶过去抓另一只了呀)。

注意我们这里从 x 1 x1 x 2 x2 的最短耗时就是从 x 1 x1 x 2 x2 的最短路径;由于 n n 不是很大,没必要用 P 5905 P5905 Johnson全源最短路的板子,因为我们用简单的 F l o y d Floyd 就可以轻松跑过。然后两重循环枚举飞猪,再用上面的方法看看它们可不可能一起被抓到即可。

同时发现,如果第 i i 只飞猪与第 j j 只飞猪能够同时被抓到,就相当于从 i i j j 连了一条单向边

Part 2

仔细研究上一个Part的最后一句话。

恍然大悟的您发现: 我们就是要找到从 1 1 k k 最长路径,并且每个节点只能经过一次(一只可爱的猪猪只能被抓一次啊),答案就是上述路径的长度(每条边的长度均为 1 1 )。而且,因为总是从编号小的点连到编号大的点,所以说不可能有环。

下面就是一个显而易见的 d p dp 了。拓扑排序没有必要,毕竟数据不大。

状态设计,即 d p i dp_i 表示从 i i k k 号节点的最长路径( k k 为飞猪的数量)。

状态转移需要想一想。每一个节点都继承了它指向的点,那么我们就可以找到它所有指向的点中 d p dp 值最大的一个,并让这个节点跟在它后面。

for (int i=head[now];i;i=e[i].next)
{
	if (e[i].to>now)  maxv=max(maxv,dp[e[i].to]);
}//找到最大值
dp[now]=maxv+1;//跟在它后面

Part 3

哎呀哎样例过不了了。怎么回事呢?

容易发现,从 1 1 号节点到 1 1 号猪猪(最早出现的猪猪)也需要时间的。

那么怎么办呢?很简单啊。

只需要加一个 0 0 号猪猪,它在 0 0 时刻, 1 1 号节点出现(即开始就能抓到它)。然后其他的正常操作就可以啦,记得带上可怜的 0 0 号猪猪哦。

而由于多了一只猪猪并且它肯定能够被抓到,所以答案还要减一。综上所述,答案为 d p 0 1 dp_0-1

Part 4

①时间复杂度: O ( m + n 3 + k 2 ) O(m+n^3+k^2)

②做题时间: 27 m i n 27min

③综合难度: 绿

④个人思路标签: 图论+最短路+Floyd+快速排序+动规dp(递推)。

注意不要开long long,搞不好这样可能会导致MLE甚至TLE。

Code

#include <bits/stdc++.h>
#define inf 5000005
using namespace std;

int n,m,k,cnt=0;
int GA[205][205],head[5005],dp[5005];

struct edge
{
	int next;
	int to;
}e[30000000];

struct node
{
	int t;
	int rt;
}pig[5005];

bool cmp(node a,node b)
{
	return a.t<b.t;
}

inline void add_edge(int u,int v)
{
	cnt++;
	e[cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
}

signed main()
{
	cin>>n>>m>>k;
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=n;j++)
		{
			if (i!=j)  GA[i][j]=inf;
			else GA[i][j]=0;
		}
	}
	for (int i=1;i<=m;i++)
	{
		int u,v,w;
		cin>>u>>v>>w;
		GA[u][v]=w;
		GA[v][u]=w;
	}
	for (int k=1;k<=n;k++)
	{
		for (int i=1;i<=n;i++)
		{
			for (int j=1;j<=n;j++)
			{
				if (GA[i][k]+GA[k][j]<GA[i][j])
				{
					GA[i][j]=GA[i][k]+GA[k][j];
					GA[j][i]=GA[i][k]+GA[k][j];
				}
			}
		}
	}
	for (int i=1;i<=k;i++)  cin>>pig[i].t>>pig[i].rt;
	
	sort(pig+1,pig+k+1,cmp);
	pig[0].t=0,pig[0].rt=1;
	
	for (int i=0;i<=k;i++)
	{
		for (int j=i+1;j<=k;j++)
		{
			if (GA[pig[i].rt][pig[j].rt]<=pig[j].t-pig[i].t)  add_edge(i,j);
		}
	}
	for (int now=k;now>=0;now--)
	{
		int maxv=0;
		for (int i=head[now];i;i=e[i].next)
		{
			if (e[i].to>now)  maxv=max(maxv,dp[e[i].to]);
		}
		dp[now]=maxv+1;
	}
	cout<<dp[0]-1<<endl;
	
	return 0;
}

特别注意

下面几题与可爱的树树有关,所以需要两个前置芝士:

①链式前向星存图与遍历方法;

②深搜优先搜索 d f s dfs

保证,下面的所有内容保证不会涉及到如考倍增LCA,树链剖分等提高组内容;但是会涉及到许多普及组内容,如树上深搜,宽搜,以及有关树的数学问题或者树上贪心等内容。

所以,大家都是能听得懂的啦~

CF1139C

Solution

如果直接计算答案,明显会很烦,想不清楚。

所以,我们决定从反面计算,并用所有的答案减去反面答案就是正面答案(宏观)。


容易发现,对于一个所有边都不是黑色的子树中,任选 k k 个数作为 a a 数组的 k k 个数一定是反面情况,因为从 a i a_i a i + 1 ( i k 1 ) a_{i+1}(i≤k-1) 的路径上一定没有经过黑色的边。

注意所有考虑的无黑边子树均为独立的;换句话说,所有上述的子树均没有公共节点或公共边。根据乘法原理可得,假设一个无黑边子树的节点数为 m m ,那么在其中选 k k 个节点作为 a a 数组有且仅有 m k m^k 种情况。

根据加法原理,得到反面答案就是 Σ m k Σ{m^k} 。由于共有 n n 个节点,那么所有答案的数量就是 n k n^k

综上所述,答案就是 n k Σ m k n^k-Σ{m^k}

一遍 d f s dfs 即可跑出所有无黑边子树的大小,带着 O ( 1 ) O(1) 时间的计算,所以时间复杂度就是 O ( n ) O(n)

Pay Attention!

①带减法的取模十分特殊,不能直接减并取模,否则答案容易变成负数。

((a-b)%mod+mod)%mod

②注意单个节点不含有边,也就意味着单个节点也是一个无黑边子树

③为了防止 d f s dfs 过于繁杂难写,本蒟蒻建议大家对于每一个节点都分别跑一遍 d f s dfs ,当然对于之前跑过的点看都不看,这样不仅使代码简单还没有增加时间复杂度。

但是,考虑到大家对树上题目的刷题量较少(我也同样),本蒟蒻详解一下本题的 d f s dfs :


inline void dfs2(int now,int fath)
//从now往下深搜找,统计含now的无黑边子树的大小
{
	tot++;
	visited[now]=1;//标记来过了
	
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath&&e[i].xv==0)  dfs2(e[i].to,now); 
		//注意我们只找无黑边,如果e[i].xv=1(红)那么我们就不深搜它孩子,否则会出现红边
	}//dfs经典操作,if (e[i].to!=fath)的意义是找它的孩子
}
inline void dfs1(int now,int fath)//使用先序遍历找每个节点的全黑子树大小
{
	tot=0;//在做dfs2前把tot清0
	if (!visited[now])  dfs2(now,fath);//如果now号节点没来过,那么就深搜
	ans=((ans-quick_power(tot,k))%mod+mod)%mod;//总的情况减去目前看到的不合法(反面)方案数,注意减法+取模的方式
	
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)  dfs1(e[i].to,now);//继续深搜它的孩子
	} 
}
......
ans=quick_power(n,k);//总共的方案数
dfs1(1,0);//深搜,求出ans(答案)的值

不懂请看注释。

最后献上 A C AC 代码~

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod=1e9+7;

int n,k,cnt=0,ans=0,tot=0;
int head[200005],visited[200005];

struct edge
{
	int next;
	int to;
	bool xv;
}e[200005];

inline void add_edge(int u,int v,int w)
{
	cnt++;
	e[cnt].to=v;
	e[cnt].xv=w;
	e[cnt].next=head[u];
	head[u]=cnt;
}

inline void dfs2(int now,int fath)
{
	tot++;
	visited[now]=1;
	
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath&&e[i].xv==0)  dfs2(e[i].to,now); 
	}
}

int quick_power(int a,int b)
{
	int res=1;
	for (;b;b=b>>1,a=(a*a)%mod)
	{
		if (b&1)  res=(res*a)%mod;
	}
	return res;
}

inline void dfs1(int now,int fath)
{
	tot=0;
	if (!visited[now])  dfs2(now,fath);
	ans=((ans-quick_power(tot,k))%mod+mod)%mod;
	
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)  dfs1(e[i].to,now); 
	} 
}

signed main()
{
	cin>>n>>k;
	for (int i=1;i<n;i++)
	{
		int u,v,w;
		cin>>u>>v>>w;
		add_edge(u,v,w);
		add_edge(v,u,w); 
	}
	ans=quick_power(n,k);
	dfs1(1,0);
	cout<<ans<<endl;
	
	return 0;
}

CF1098A

Solution

简单的贪心。

我们需要在让答案存在的情况下,尽可能让深度小的节点的点权大,这样才能"造福"它的后代。原因显然,深度小的节点能够影响更多的节点(它的孩子)。如果它的点权变大那么它的许多子节点的点权就可以变小,也就可以答案更优。

但是,不能让深度小的节点的点权太大,否则它孩子的点权就变成负的了,这是绝对不可以的。换句话说,我们对于一个非叶节点的点权应该设定为它所有孩子的 s i s_i 的最小值。

如果最终构造出来的某个节点的点权为负,那么就应该输出-1;注意到这已经尽可能让所有点权非负了,如果还不行就说明无解

上AC代码~

#include <bits/stdc++.h>
#define int long long
using namespace std;

int n,ans=0,cnt=0;
int head[100005],s[100005],a[100005],minv[100005],ns[100005];

struct edge
{
	int next;
	int to;
}e[200005];

inline void add_edge(int u,int v)
{
	cnt++;
	e[cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
}

inline void dfs(int now,int fath)
{
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)  dfs(e[i].to,now);
	}
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)  minv[now]=min(minv[now],minv[e[i].to]);
	}
	minv[now]=min(minv[now],s[now]);
}

inline void dfs2(int now,int fath)
{
	if (s[now]!=1000000007)
	{
		a[now]=s[now]-ns[fath];
		ns[now]=ns[fath]+a[now];
	}
	else
	{
		int num;
		if (minv[now]==1e9+7)  num=0;
		else num=minv[now]-ns[fath];
		
		a[now]=num;
		ns[now]=ns[fath]+num;
	}
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)  dfs2(e[i].to,now);
	}
}

signed main()
{
	cin>>n;
	for (int i=2;i<=n;i++)
	{
		int u;
		cin>>u;
		add_edge(u,i);
		add_edge(i,u);
	}
	for (int i=1;i<=n;i++)
	{
		cin>>s[i];
		if (s[i]==-1)  s[i]=1e9+7;
	}
	for (int i=1;i<=n;i++)  minv[i]=1e9+8;
	
	dfs(1,0);
	dfs2(1,0);
	
	for (int i=1;i<=n;i++)
	{
		if (a[i]<0)  return cout<<-1<<endl,0; 
	}
	for (int i=1;i<=n;i++)  ans+=a[i];
	cout<<ans<<endl;
	
	return 0;
}

为了做下面的几个练习题,补上两个小芝士~

树的直径

树的直径,即树中最长的链。换句话说,就是树中任意两点之间的距离的最大值

这里推崇两种办法:

d f s dfs 法。

条件: 无负权路,给定的图为树。

首先,从 1 1 号节点开始,向下进行 d f s dfs ;然后找到离 1 1 号节点最远的那个节点 l l 。注意 l l 号节点一定为叶子节点,因为每条路都可能为直径做出贡献。

然后,换 l l 号节点为根,再深搜一遍,找到离 l l 号节点距离最远的 r r 好节点,从 l l 号节点到 r r 号节点之间的路径就是树的最长链(直径), l l r r 就是该直径的两端。

d p dp 法。

条件: 给定的图为树。

状态设计: d p i dp_i 表示以 i i 为根的子树中,以 i i 为直径一端的最长链的长度

首先,从 1 1 号节点开始进行深搜。假设目前 i i 号节点的孩子已经深搜过了(有了 d p dp 值),那么我们就可以推出 d p i dp_i 的值,并更新最长链的长度。

首先,显然 d p i = m a x ( d p s o n i + e d g e ( i , s o n i ) ) dp_i=max(dp_{son_i}+edge(i,{son_i})) ,其中 s o n i son_i 表示 i i 的孩子节点, e d g e ( u , v ) edge(u,v) 表示两点之间的最短路径。

同时,我们也要求出,在以 i i 为根的子树中,经过 i i 的最长链,并用这个值来更新直径的长度。显然, d = m a x ( d , d p i + d p s o n i + e d g e ( i , s o n i ) ) d=max(d,dp_i+dp_{son_i}+edge(i,son_i)) 。即,描述了到 s o n i son_i 的最长链,再经过 ( i , s o n i ) (i,son_i) 这条边,然后再继续选择另外一个孩子向下走的这个过程。

本蒟蒻将会白版演示一下:

for (int i=head[now];i;i=e[i].next)
	{
        if (e[i].to!=fath)
        {
        	find_diameter(e[i].to,now);
        	len2=max(len2,dp[now]+dp[e[i].to]+e[i].dis);
        	dp[now]=max(dp[now],dp[e[i].to]+e[i].dis);	
        }
    }

遍历两点之间的路径

假设我们要遍历的是从 l l r r 的路径。

那么,我们可以暂时设 l l 为根,然后先深搜一遍, w i t h i with_i 记录下以 I I 为根的子树中是否含有 r r 这个节点。如果含有则记 w i t h i = 1 with_i=1 ,否则 w i t h i = 0 with_i=0

然后,从 l l 号节点开始向下搜,每次向着唯一一个使得 w i t h s = 1 with_s=1 的孩子 s s ,直到到达了 r r 号节点为止。

inline void dfs3(int now,int fath)
{
	int flag=0;
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)
		{
			dfs3(e[i].to,now);
			if (with[e[i].to]==1)  flag=1;
		}
	}
	if (flag==1||now==r)  with[now]=1;
}

w i t h i with_i 记录下以 I I 为根的子树中是否含有 r r 这个节点。如果含有则记 w i t h i = 1 with_i=1 ,否则 w i t h i = 0 with_i=0

inline void dfs4(int now,int fath)
{
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)
		{
			if (with[e[i].to]==1)  dfs4(e[i].to,now);
			}
		}
	}
}

直接从 l l 深搜下去,每次向着唯一一个使得 w i t h s = 1 with_s=1 的孩子 s s ,直到到达了 r r 号节点为止。

注意,这种方法并不简便,用 m a p map 做更快;但是问题在于,用 m a p map 或倍增 L C A LCA 的时间复杂度可以被我卡到 n l o g 2 n nlog_2 n ;而使用记录+深搜的方法,在极端情况下时间复杂度为 O ( n ) O(n)

所以,不要害怕繁杂,毕竟时间复杂度十分重要。


CF219D

作为练习题。

ABC133E

作为练习题。

P1099

作为练习题。


CF969B

值得一说,顺便与大家一起了解一下期望

期望是个什么东西?可以吃吗?

期望,是指所有方案的答案之和,除以方案的数量。

比如,在 2 5 2-5 中任意选一个正整数的期望为 3.5 3.5 ;因为所有方案的和为 2 + 3 + 4 + 5 = 14 2+3+4+5=14 ,且方案数为 5 2 + 1 = 4 5-2+1=4 ,所以期望就是 14 ÷ 4 = 3.5 14÷4=3.5

在期望题中,经常要这么搞:

①分别找到所有方案的和与方案的数量;

②求出每一个方案的答案乘上它出现的概率 之和。

由于经验严重不足,所以本蒟蒻只能说这两个QAQ

Solution

分别考虑每一个的被访问编号的期望,并使用 d f s dfs 有顺序地求出每一个节点的上述的值。

设节点 n o w now 的访问编号为 d p n o w dp_{now} ,且 f a t h fath 号节点为 n o w now 的父节点。容易发现,如果 n o w now 的一位兄弟 b r o bro 被先访问了,那么它就让 d p n o w dp_{now} 加上了 s i z e b r o size_{bro} ,其中 s i z e b r o size_{bro} 表示 b r o bro 的子树大小。

同时要注意, d p n o w dp_{now} d p f a t h dp_{fath} 转移而来;同时这里运用经验②(每一个方案的答案乘上它出现的概率之和),列出状态转移公式:

d p n o w = d p f a t h + 1 + Σ i = 1 t p b r o i s i z e b r o i dp_{now}=dp_{fath}+1+Σ_{i=1}^t p_{bro_i}size_{bro_i}

其中 t t 表示 n o w now 节点的兄弟数, b r o i bro_i 表示 n o w now 节点的地 i i 个兄弟, p b r o i p_{bro_i} 表示 b r o i bro_i n o w now 号节点之前被访问的概率。之所以要加上1,是因为它不可能与它父亲同时被访问。

发现,如果直接计算这个式子,时间复杂度直接上天。于是,我们看一下,能不能预处理出一些值呢?

首先,第一遍 d f s dfs 我们可以预处理出每个节点的 s i z e size 值。

同时,发现 p b r o i p_{bro_i} 的值都是 0.5 0.5 !为什么?因为某个节点的兄弟一定在 n o w now 号节点前被访问,或在 n o w now 号节点后被访问;因此它在 n o w now 之前被访问的概率就是 1 2 \frac 1 2 ,即 0.5 0.5

综上所述,递推式就是:

d p n o w = d p f a t h + 1 + Σ i = 1 t s i z e b r o i 2 dp_{now}=dp_{fath}+1+\frac {Σ_{i=1}^t size_{bro_i}} 2

那么怎么 O ( 1 ) O(1) 转移呢?

显然, Σ i = 1 t s i z e b r o i = s i z e f a t h s i z e n o w 1 Σ_{i=1}^t size_{bro_i}=size_{fath}-size_{now}-1 ,再带入原式:

d p n o w = d p f a t h + 1 + s i z e f a t h s i z e n o w 1 2 dp_{now}=dp_{fath}+1+\frac {size_{fath}-size_{now}-1} 2

这就是最终得到的递推式,可以 O ( 1 ) O(1) 转移。

时间复杂度 O ( n ) O(n)

花絮

由于语言组织能力有一定问题,本蒟蒻决定模仿白鲟大佬写的题解,加上自己的理解与自己的解释,使正解更容易被理解与接受。

这是否会成为第一个您完全搞懂且最终一遍 A C AC 的紫题?

Code

#include <bits/stdc++.h>
#define int long long
using namespace std;

int n,cnt=0;
int head[200005],size[200005];
double ans[200005];

struct edge
{
	int next;
	int to;
}e[200005];

inline void add_edge(int u,int v)
{
	cnt++;
	e[cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
}

inline void dfs(int now,int fath)
{
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)  dfs(e[i].to,now),size[now]+=size[e[i].to];
	}
}

inline void dfs2(int now,int fath)
{
	for (int i=head[now];i;i=e[i].next)
	{
		if (e[i].to!=fath)
		{
			ans[e[i].to]=ans[now]+(0.50*(size[now]-size[e[i].to]-1))+1.00;
			dfs2(e[i].to,now);
		}
	}
}

signed main()
{
	cin>>n;
	for (int i=2;i<=n;i++)
	{
		int v;
		cin>>v;
		add_edge(i,v);
		add_edge(v,i);
	}
	for (int i=1;i<=n;i++)  size[i]=1;
	
	ans[1]=1.00;
	dfs(1,0);
	dfs2(1,0);
	
	for (int i=1;i<=n;i++)  cout<<ans[i]<<' ';
	cout<<endl;
	
	return 0;
}

P1297

Description

i i 题有 a i a_i 个选项。 C h e r r t Cherrt 做对了每一题,但是在最终填答题卡的时候填挫了,即第 i i 题的答案填到了第 i + 1 i+1 题上。特别的,第 n n 题的答案填到了第 1 1 题上 (我也是醉了)

求可怜的 C h e r r t Cherrt 的正确题数的期望。

Solution

容易得到,做对题数的期望就是每题做对的概率之和。

做对一题当且仅当本题的答案与上一题的答案相同。因此,第 i i 题正确的概率为 m i n ( a i , a i 1 ) a i × a i 1 \frac {min(a_i,a_{i-1})} {a_i×a_{i-1}} 。分母为选项的组合的数量,分子为第 i i 空正确的答案组合的数量。

a 0 = a n a_0=a_n ,那么所以答案就是 i = 1 n m i n ( a i , a i 1 ) a i × a i 1 ∏_{i=1}^n \frac {min(a_i,a_{i-1})} {a_i×a_{i-1}}

提前构造出数组 a a 即可。时间复杂度为 O ( n ) O(n)

Code

#include <bits/stdc++.h>
#define int long long
using namespace std;

int n,A,B,C;
double ans=0.00;
int a[10000005];

signed main()
{
	cin>>n>>A>>B>>C>>a[1];
	for (int i=2;i<=n;i++)  a[i]=(a[i-1]*A+B)%100000001;
	for (int i=1;i<=n;i++)  a[i]=a[i]%C+1;//3 2 4
	a[0]=a[n];
	for (int i=1;i<=n;i++)
	{
		double first=min(a[i],a[i-1])*1.00,second=a[i]*a[i-1];
		ans+=first/second;
	}
	cout<<fixed<<setprecision(3)<<ans<<endl;
	
	return 0;
}

既然扯到期望了,那么我们就说说期望的几道例题吧。

P6154

Description

给定一个 n n 个点, m m 有向边无环图,求出路径长度的期望。

Solution

本题有套路式做法,即求出所有路径的长度之和与路径的条数。为了求这两个量,我们使用 d p dp ,即状态设计 t o t l i totl_i 表示目前发现到 i i 点的所有路径的长度之和, c n t l i cntl_i 表示目前发现到 i i 点的路径的数量。

在大部分的图形dp或树形dp中,任何一个节点的 d p dp 值均转移自源于与它相邻的节点。假设我们现在想要得到 t o t l n o w totl_{now} c n t l n o w cntl_{now} 的值,那么它们必须由 y y 推到而来( y y n o w now 有一条无向边,其中 y y 连向 n o w now )。所以得到状态转移:

c n t l n o w = ( c n t l n o w + c n t y ) cntl_{now}=(cntl_{now}+cnt_y)
t o t l n o w = ( t o t l n o w + t o t l y + c n t l y ) totl_{now}=(totl_{now}+totl_y+cntl_y)

①表示, c n t l n o w cntl_{now} 的值要加上被它连接的那个点( y y )的 c n t l cntl 值,因为 c n t l y cntl_y 计算的那些路径均可以通过加一条边 y x y→x 连到 x x ,所以有 + c n t y +cnt_y 这一步。

②表示, t o t l n o w totl_{now} 的值要加上一些东西。甚么东西?即, t o t l n o w totl_{now} 计算的路径均可以通过加一条边 y x y→x 连到 x x ,这样每条路径的长度都变长了1;因此不仅要加上 t o t l y totl_y ,还要加上 c n t l y cntl_y

事实上,您可以叫它记忆化搜索

Code

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353;

int n,m,cnt=0,sumv=0,num=0;
int head[1000005],totl[1000005],cntl[1000005];

struct edge
{
	int next;
	int to;
}e[2000005];

inline void add_edge(int u,int v)
{
	cnt++;
	e[cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
}

inline void dfs(int now)
{
	if (cntl[now]!=0)  return;
	cntl[now]=1;
	for (int i=head[now];i;i=e[i].next)
	{
		dfs(e[i].to);
		cntl[now]=(cntl[now]+cntl[e[i].to])%mod;
		totl[now]=(totl[now]+totl[e[i].to]+cntl[e[i].to])%mod;
	}
}

int quick_power(int a,int b)
{
	if (b==0)  return 1;
	
	int res=1;
	for (;b;b=b>>1,a=(a*a)%mod)
	{
		if (b&1)  res=(res*a)%mod;
	}
	return res;
}

inline int divide(int a,int b)
{
	return (a*quick_power(b,mod-2))%mod;
}

signed main()
{
	cin>>n>>m;
	for (int i=1;i<=m;i++)
	{
		int u,v;
		cin>>u>>v;
		add_edge(u,v);
	}
	for (int i=1;i<=n;i++)
	{
		if (cntl[i]==0)  dfs(i);
	}
	for (int i=1;i<=n;i++)  sumv=(sumv+totl[i])%mod;
	for (int i=1;i<=n;i++)  num=(num+cntl[i])%mod;
	cout<<divide(sumv,num)<<endl;
	
	return 0;
}

猜你喜欢

转载自blog.csdn.net/Cherrt/article/details/106171642