Codeforces Round 926 (Div. 2)(A,B,C,D,E,F)

这场还是很有含金量的,B题开始就有难度了,B是个推结论的题,C要推结论然后递推,D题是有点难的树上DP(主要是状态转移方程不好写),E题是个二进制预处理然后状压DP,F题是个数论(把树映射成中序遍历dfs序,然后跑隔板法),很推荐打一打这场。

比赛链接


A. Sasha and the Beautiful Array

题意:

Sasha决定送给女友一个数组 a 1 , a 2 , … , a n a_1, a_2, \ldots, a_n a1,a2,,an 。他发现女友会评估数组的美丽值: 对所有整数 i i i 2 2 2 n n n ( a i − a i − 1 ) (a_i - a_{i - 1}) (aiai1) 的和。

请帮助Sasha,告诉他,如果他能以任何方式重新排列数组 a a a 中的元素,他能得到的最大数组美丽值是多少。

思路:

其实跟差分数组的思想差不多,推一下式子就能发现,这个美丽值其实就是 a n − a 1 a_n - a_1 ana1,所以把最大数放在位置 n n n 上,最小数放在位置 1 1 1 上。最大值和最小值的差值就是答案。

code:

#include <iostream>
#include <cstdio>
using namespace std;
const int inf=2e9;

int T,n;

int main(){
	cin>>T;
	while(T--){
		cin>>n;
		int minn=inf,maxx=-inf;
		for(int i=1,t;i<=n;i++){
			cin>>t;
			minn=min(minn,t);
			maxx=max(maxx,t);
		}
		cout<<maxx-minn<<endl;
	}
	return 0;
} 

B. Sasha and the Drawing

题意:

自从在幼儿园时,Sasha就喜欢上了一个女孩。因此,他想给她画一幅画,吸引她的注意。

作为一幅画,他决定画一个大小为 n × n n \times n n×n 的正方形网格,在网格中给一些单元格涂上颜色。但是给单元格上色很困难,所以他想给尽可能少的单元格上色。但同时,他又希望至少 k k k 个对角线上至少有一个着色的单元格。请注意,大小为 n × n n \times n n×n 的正方形网格共有 4 n − 2 4n - 2 4n2 条对角线。

帮助小萨沙让女孩爱上他,并告诉他需要涂色的最少格子数。

思路:

这个题主要是得找张纸划拉划拉,推结论。

我们假设有个 3 ∗ 3 3*3 33 的矩形,然后把格子缩成点,先看从左上到右下的所有对角线,如图。
在这里插入图片描述

在这里插入图片描述

再顺时针旋转45度:
在这里插入图片描述

我们想要尽可能少的涂色,那就要求每次涂色占到尽可能多的没有涂色的对角线。因为一个格子最多就在两条对角线上(一条从左上到右下的,一条从右上到左下的)。所以在上图中,就是选多个点使得每个点尽量占到一条竖直的线和水平的线(水平的没画)。

从最左边的 7 7 7 号点所在竖线开始看,显然可以占到竖直的和水平的线各一个,再看 4 , 8 4,8 4,8 所在竖线,它们和 2 , 6 2,6 2,6 点所在的横线会产生冲突,不过不打紧,如果我们选 4 4 4 号点,那么另一条选择 6 6 6 号点,如果我们选 8 8 8 号点,那么另一条选择 2 2 2 号点,就行了。类似的,对中间所有的对称的线都可以这样操作。

但是到了最后一条竖线,也就是 3 3 3 号点所在竖线的时候,它无论怎么选都只能产生一次贡献。同理横线,如果在 1 , 5 , 9 1,5,9 1,5,9 这条竖线的选取中,选取了 5 5 5 ,那么 9 9 9 所在横线就没人选了,同理选取了 9 9 9 ,那么 5 5 5 所在横线就没人选了,假设矩形边长为 n n n,那么一个方向的对角线条数有 2 ∗ n − 1 2*n-1 2n1 条,选取了 2 ∗ n − 1 − 1 2*n-1-1 2n11 个点,每个点占两个对角线,这时就拿到了 2 ∗ ( 2 ∗ n − 1 − 1 ) = 4 ∗ n − 4 2*(2*n-1-1)=4*n-4 2(2n11)=4n4 条对角线,还差最后两条没有选,差的就是这两条,这两条要选点的话,选一个点只能产生一次贡献。

所以分类讨论,如果 k ≤ 4 ∗ n − 4 k\le 4*n-4 k4n4 ,答案就是 ⌈ k 2 ⌉ \left\lceil \dfrac k 2 \right\rceil 2k,如果 4 ∗ n − 4 < k ≤ 4 ∗ n − 2 4*n-4\lt k\le 4*n-2 4n4<k4n2,答案就是 2 ∗ n − 2 + ( k − 4 ∗ n + 4 ) 2*n-2+(k-4*n+4) 2n2+(k4n+4)

code:

#include <iostream>
#include <cstdio>
using namespace std;

int T,n,k;

int main(){
	cin>>T;
	while(T--){
		cin>>n>>k;
		if(k<=4*n-4)cout<<(k+1)/2<<endl;
		else cout<<2*n-2+(k-4*n+4)<<endl;
	}
	return 0;
}

其实可以发现只有 k = 4 ∗ n − 2 k=4*n-2 k=4n2 时答案会比 ⌈ k 2 ⌉ \left\lceil \dfrac k 2 \right\rceil 2k 多1,其他时候是一样的,所以还可以写的简单些。

#include <iostream>
#include <cstdio>
using namespace std;

int T,n,k;

int main(){
	cin>>T;
	while(T--){
		cin>>n>>k;
		cout<<(k+1)/2+(k==4*n-2)<<endl;
	}
	return 0;
}

C. Sasha and the Casino

题意:

Sasha决定把最好的手提包送给他的女朋友,但不幸的是,这个手提包非常昂贵。因此,Sasha想赚取它。在网上查看了赚钱技巧后,他决定去赌场。

Sasha知道赌场的运作规则如下。如果萨沙下注 y y y 个硬币(其中 y y y 为正整数),那么如果赢了,他将获得 y ⋅ k y \cdot k yk 个硬币(即他的硬币数量将增加 y ⋅ ( k − 1 ) y \cdot (k - 1) y(k1) )。如果输了,他将输掉全部赌注( 即他的硬币数量将减少 y y y )。

请注意,投注金额必须始终是一个正( > 0 \gt 0 >0 )整数,并且不能超过Sasha当前的硬币数量。

Sasha还知道赌场有一个促销活动:他不能连续输超过 x x x 次。

最初,Sasha有 a a a 枚硬币。他想知道自己是否可以下注保证赢取任意数量的硬币。换句话说,对于任意整数 n n n ,Sasha是否可以通过合理下注,且不违背上述规则,在某个时刻他可以拥有至少 n n n 个硬币。

思路:

有点东西。手玩样例,比如第三个样例,下注策略是 1   2   4   a l l i n 1\ 2\ 4\ allin 1 2 4 allin

玩一会就应该发现端倪了。赢了翻 k k k 倍,输 x x x 次就保底,手里有 a a a 块钱。每次下注赢了之后奖池刷新。

因为我们要保证一定可以拥有任意多的钱(其实就是赚到钱),所以即使在最坏情况下我们也得赚到钱。比如第三个样例 k = 2 , x = 3 , a = 15 k=2,x=3,a=15 k=2,x=3,a=15,我们一开始下注 1 1 1 块钱,输了就失去一块钱,但是我们离保底近了一步,赢了就皆大欢喜,白赚一块,然后刷新保底重新下注。

我们如果上次输了,这次如果下注 1 1 1 块,赢了就只能拿到两块钱,并没有赚到钱,如果运气不好就会一直输一次赢一次,然后钱数永远不变。这就赚不到钱了。所以我们第二次下注两块,赢了就能赚到两块钱,输了就离保底更近一步。

如果第二次下注输了,同理,如果这次下注两块,我们三次总投入五块,赢了就只能得到四块,如果后面循环这种最坏情况,我们就会一直亏钱。如果这次下注三块,我们三次总投入六块,赢了就只能得到六块,如果后面循环这种最坏情况,我们就会一直赚不到钱。所以我们需要下注四块,这样投入七块,赢了虽然丢了保底但也能赚到,输了能能保底了。

如果第三次也输了,那就直接all in,反正一定赢,这时我们手上还剩 8 8 8 块,能得到 16 16 16 块,这样也赚到了。

手玩一下过程其实就很明了了,下注策略就是:我们要保证中间任何一次下注赢了都可以赚到钱,以及最后保底all in的时候能赚到钱就行了。这就需要你的初始钱包够硬气。

所以我们可以模拟这种下注策略,然后检查每个时刻时候钱包还够不够。以及最后all in的时候能不能赚到就行了。

code:

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=105;
typedef long long ll; 

ll T,k,x,a;//翻k倍 x次保底 初始 

ll st[maxn],lose[maxn];

int main(){
	cin>>T;
	while(T--){
		cin>>k>>x>>a;
		bool f=false;
		st[1]=1;
		lose[1]=1;
		for(int i=2;i<=x;i++){
			st[i]=(lose[i-1])/(k-1)+1;
			lose[i]=lose[i-1]+st[i];
			if(lose[i]>a){
				f=true;
				break;
			}
		}
		if(f || (a-lose[x])*k<=a)puts("NO");
		else puts("YES");
	}
	return 0;
}

D. Sasha and a Walk in the City

题意:

Sasha想和他的女朋友在城市里散步。城市由 n n n 个路口组成,编号从 1 1 1 n n n 。其中一些路口由道路连接,从任何一个路口出发,都有一条简单的路径 † ^{\dagger} 通往其他路口。换句话说,十字路口和它们之间的道路组成了一棵树。

有些交叉路口被认为是危险的。由于在城市中独自行走很不安全,所以Sasha不想在行走过程中经过三个或三个以上的危险路口。

如果满足以下条件,Sasha就会将 一组 路口称为好路口:

  • 如果在城市中这组交叉路口是危险的,那么城市中的任何一条简单路径都包含不超过两个危险的交叉路口。

然而,Sasha并不知道哪些交叉路口是危险的,因此他感兴趣的是城市中不同的好交叉路口集的数量。由于这个数字可能非常大,因此输出它的模数 998   244   353 998\,244\,353 998244353

† ^{\dagger} 简单路径是指最多经过每个交叉路口一次的路径。

思路:

这题面绕的很,说白了就是:就是从树中选一些点。 把他们视为危险点。问有多少种 选点组合能保证 “树上不存在某个路径上有三个及以上危险点” 。

首先先想明白怎么样选点能保证 “树上不存在某个路径上有三个及以上危险点”。对一个树上路径,我们可以给它两边都一直延伸到某个叶子节点,如果这个长路径没有三个及以上危险点,那么这个路径就一定没有危险点,所以我们可以去保证任意两条根节点到叶子节点的链上没有三个及以上危险点(根节点为危险点的,两个链只算一次),这样就可以保证任意一条路径都满足要求了。

任取两条根到叶子节点的链需要经过两个儿子节点,换句话说,根到叶子节点的链其实就是儿子到叶子的链,最后再加上根。对这个儿子节点,我们肯定走这个儿子节点下包含最多危险节点的链,如果这个最危险的链都能满足条件存在,那么其他链也能满足条件。

d p [ u ] [ i ] dp[u][i] dp[u][i] 表示以 u u u 为根节点的子树包含最多危险点的链(假设就叫最危险链) 包含 i i i 个危险点的方法数。

v 1 , v 2 , … , v k v_1,v_2,\dots,v_k v1,v2,,vk 是节点 u u u 的儿子节点,考虑如何转移。

  1. d p [ u ] [ 0 ] dp[u][0] dp[u][0],肯定儿子节点的子树的所有点都不能包含危险点,这个点本身也不能是危险点,答案数就是 d p [ u ] [ 0 ] = 1 dp[u][0]=1 dp[u][0]=1
  2. d p [ u ] [ 1 ] dp[u][1] dp[u][1],两种情况:
    1. 这个点 u u u 不是危险点:至少一个子树的最危险链包含1个危险点,其他的子树的最危险链可以有0个或者1个危险点,每个子树的情况相加。枚举来算比较花时间,可以容斥定理来算:每个子树的最危险链可以有0个和1个危险点的情况的乘积减去所有子树的最危险链都为0个危险点的情况数,也就是 ∏ i = 1 k ( d p [ v i ] [ 0 ] + d p [ v i ] [ 1 ] ) − ∏ i = 1 k d p [ v i ] [ 0 ] = ∏ i = 1 k ( d p [ v i ] [ 0 ] + d p [ v i ] [ 1 ] ) − 1 \prod_{i=1}^k(dp[v_i][0]+dp[v_i][1])-\prod_{i=1}^kdp[v_i][0]=\prod_{i=1}^k(dp[v_i][0]+dp[v_i][1])-1 i=1k(dp[vi][0]+dp[vi][1])i=1kdp[vi][0]=i=1k(dp[vi][0]+dp[vi][1])1
    2. 这个点 u u u 是危险点:所有子树都不能有危险点,也就是子树最危险链包含0个危险点,这种情况只有 ∏ i = 1 k d p [ v i ] [ 0 ] = 1 \prod_{i=1}^kdp[v_i][0]=1 i=1kdp[vi][0]=1
  3. d p [ u ] [ 2 ] dp[u][2] dp[u][2],两种情况:
    1. 这个点 u u u 不是危险点:一个子树的最危险链包含两个危险点,其他子树保持0个危险点,情况数为 d p [ v 1 ] [ 2 ] + d p [ v 2 ] [ 2 ] + ⋯ + d p [ v k ] [ 2 ] = ∑ i = 1 k d p [ v i ] [ 2 ] dp[v_1][2]+dp[v_2][2]+\dots+dp[v_k][2]=\sum_{i=1}^kdp[v_i][2] dp[v1][2]+dp[v2][2]++dp[vk][2]=i=1kdp[vi][2]
    2. 这个点 u u u 是危险点:一个子树的最危险链包含一个危险点,其他子树保持0个危险点,情况数为 d p [ v 1 ] [ 1 ] + d p [ v 2 ] [ 1 ] + ⋯ + d p [ v k ] [ 1 ] = ∑ i = 1 k d p [ v i ] [ 1 ] dp[v_1][1]+dp[v_2][1]+\dots+dp[v_k][1]=\sum_{i=1}^kdp[v_i][1] dp[v1][1]+dp[v2][1]++dp[vk][1]=i=1kdp[vi][1]

然后dfs跑树上DP即可。假设起点是 1 1 1,答案就是 d p [ 1 ] [ 0 ] + d p [ 1 ] [ 1 ] + d p [ 1 ] [ 2 ] dp[1][0]+dp[1][1]+dp[1][2] dp[1][0]+dp[1][1]+dp[1][2]

code:

为了方便想,我在dfs时使用 n 0 , n 1 , n 2 n0,n1,n2 n0,n1,n2 分别表示不选取根节点 u u u 时,最危险链包含 0 , 1 , 2 0,1,2 0,1,2 个危险点的方法数, x 2 x2 x2 表示选取根节点 u u u 时,最危险链包含 2 2 2 个危险点的方法数。

实际上在累加或累乘时,这几个变量分别处理的是这几个式子:
n 0 = 1 n0=1 n0=1
n 1 = ∏ i = 1 k ( d p [ v i ] [ 0 ] + d p [ v i ] [ 1 ] ) n1=\prod_{i=1}^k(dp[v_i][0]+dp[v_i][1]) n1=i=1k(dp[vi][0]+dp[vi][1]) (计算结束后根据容斥定理减去 n 0 = 1 n0=1 n0=1
n 2 = ∑ i = 1 k d p [ v i ] [ 2 ] n2=\sum_{i=1}^kdp[v_i][2] n2=i=1kdp[vi][2]
x 2 = ∑ i = 1 k d p [ v i ] [ 1 ] x2=\sum_{i=1}^kdp[v_i][1] x2=i=1kdp[vi][1]

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=3e5+5;
typedef long long ll;
const ll mod=998244353;

int T,n;

ll dp[maxn][5];
//i点为根的子树所有到叶子的链上最多包含j个危险路口的方法数 
int head[maxn],cnt;
struct edge{
	int v,nxt;
}e[maxn<<1];
void add(int u,int v){
	e[++cnt].v=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}
void init(){
	cnt=0;
	for(int i=1;i<=n;i++)
		head[i]=dp[i][0]=dp[i][1]=dp[i][2]=0;
}

void dfs(int u,int rt){
	dp[u][0]=1;
	ll n0,n1,n2,x2;//儿子子树上最多 
	n0=n1=1;
	n2=x2=0;
	for(int i=head[u],v;i;i=e[i].nxt){
		v=e[i].v;
		if(v==rt)continue;
		dfs(v,u);
		n1=n1*(dp[v][1]+dp[v][0])%mod;
		n2=(n2+dp[v][2])%mod;
		x2=(x2+dp[v][1])%mod;
	}
	
	n1-=n0;
	dp[u][1]=(n1+n0)%mod;
	dp[u][2]=(n2+x2)%mod;
}

int main(){
	cin>>T;
	while(T--){
		cin>>n;
		init();
		for(int i=1,u,v;i<n;i++){
			cin>>u>>v;
			add(u,v);
			add(v,u);
		}
		dfs(1,-1);
		cout<<(dp[1][0]+dp[1][1]+dp[1][2])%mod<<endl;
	}
	return 0;
}

E. Sasha and the Happy Tree Cutting

题意:

Sasha获得了一棵有 n n n 个顶点的树 † ^{\dagger} ,作为他又一次赢得比赛的奖品。然而,庆祝完胜利回家后,他发现树的某些部分不见了。Sasha记得他给这棵树的一些边涂了颜色。他可以肯定,在顶点 k k k 对顶点 ( a 1 , b 1 ) , … , ( a k , b k ) (a_1, b_1), \ldots, (a_k, b_k) (a1,b1),,(ak,bk) 中,他至少为顶点 a i a_i ai b i b_i bi 之间的简单路径 ‡ ^{\ddagger} 上的一条边涂了颜色。

Sasha不记得他到底给多少条边涂了颜色,因此他请你告诉他,为了满足上述条件,他至少可以给多少条边涂颜色。

† ^{\dagger} 一棵树是没有循环的不定向连接图。

‡ ^{\ddagger} 简单路径是指每个顶点最多经过一次的路径。

思路:

F感觉比E简单,而且过的人也比E多。E题感觉是个状压DP。

k很小,只有20,而且题目还专门说了是 2 k ≤ 2 20 2^k\le 2^{20} 2k220。估计是暗示咱们去状压这个东西。具体来说,对每个边,用一个二进制数的第 i i i 位记录一下它是不是第 i i i 个路径上含有的边。那么我们在涂这个边的时候,就知道我们涂好了哪几个路径。把每个边的这个二进制数当作一个物品,之后跑DP就行了。

具体做法首先是标准的链式前向星存边,然后找一个路径上的所有边并进行标记。这里因为 k k k 很小,直接dfs只有 20 n 20n 20n 次运算,直接暴力dfs找边即可,然后找到之后返回时进行标记,这里可以用链式前向星存边的特性:第 i i i 个遍存放在边的数组的第 2 ∗ i − 1 2*i-1 2i1 2 ∗ i 2*i 2i 个位置上。

用一个数组来存储每个边的路径信息。因为重复物品没有用,而且边最多可以有 1 0 5 − 1 10^5-1 1051,状态最多有 2 20 ≈ 1 0 6 2^{20}\approx10^6 220106 种,直接跑背包DP是 O ( 1 0 5 ∗ 1 0 6 ) O(10^5*10^6) O(105106) 的,时间会爆。所以对物品进行去重,之后对剩下的物品跑01背包就行了。就做完了。

另外说点:为什么去重之后时间复杂度就达标了?去重之后物品能剩下几个?我也感觉成谜,这里给出我的一点想法,如果有大佬会算欢迎指点。

假设树上已经有了 x x x 条路径,这里我们再向上添加一条路径,使得不同的边(不同指的是属于路径不同,就是上面说的那个二进制数不同)变化量最大。

首先如果要变化量最大,显然 x x x 条路径应该都经过一条边,然后我们新增的这个路径也经过这个边。在这个边的左边,新增的路径沿边向下延伸,因为在经过一个点之后,先前的 x x x 个路径一定会分出去1股或多股,要变化量最大,一次只分出去1股,这样左边最多会有 x x x 次就会把原来的路径分出去,只剩下新增路径,左边就有了 x x x 条不同的边。右边可以使用另一种分股方式,由于最后只剩下新增路径的和左边重复了,所以有 x − 1 x-1 x1 种不同的边,总的就是最多 2 ∗ x 2*x 2x 条不同的边。

因此上面 k = 20 k=20 k=20,去完重后最多剩下 40 40 40 个不同的物品,背包就是 O ( 40 ∗ 2 20 ≈ 4 ∗ 1 0 7 ) O(40*2^{20}\approx4*10^7) O(402204107) 的复杂度。代码跑了500ms,应该大体上是吻合的。

code:

#include <iostream>
#include <cstdio>
#include <stack>
#include <set>
using namespace std;
const int maxn=1e5+5;
const int inf=1e9;

int T,n,k;

int head[maxn],cnt;
struct edge{
	int v,nxt;
}e[maxn<<1];
void add(int u,int v){
	e[++cnt].v=v;
	e[cnt].nxt=head[u];
	head[u]=cnt;
}

int a[maxn],dp[maxn<<4];//每条边的位掩码 到st状态时的最小边 

bool dfs(int u,int lst,int ed,int x){
	if(u==ed)return true;
	
	for(int i=head[u],v;i;i=e[i].nxt){
		v=e[i].v;
		if(v==lst)continue;
		if(dfs(v,u,ed,x)){
			a[(i+1)>>1]|=x;
			return true;
		}
	}
	return false;
}
void pb(int x){
	stack<int> s;
	for(int i=1;i<=k;i++){
		s.push(x%2);
		x/=2;
	}
	while(!s.empty()){
		cout<<s.top();
		s.pop();
	}
	puts("");
}

int main(){
	cin>>T;
	while(T--){
		cin>>n;
		cnt=0;
		for(int i=1;i<=n;i++)
			head[i]=a[i]=0;
		for(int i=1,u,v;i<n;i++){
			cin>>u>>v;
			add(u,v);
			add(v,u);
		}
		cin>>k;
		for(int i=1;i<=((1<<k)-1);i++)
			dp[i]=inf;
		for(int i=0,u,v;i<k;i++){
			cin>>u>>v;
			dfs(u,-1,v,(1<<i));
		}
		
//		puts("");
//		for(int i=1;i<n;i++)
//			pb(a[i]);
//		puts("");
		
		//处理好了n-1个物品
		set<int> S;
		for(int i=1;i<n;i++)
			S.insert(a[i]);
		for(int j=0;j<=((1<<k)-1);j++){
			for(auto x:S)
				dp[j|x]=min(dp[j|x],dp[j]+1);
		}
		cout<<dp[(1<<k)-1]<<endl;
	}
	return 0;
} 

F. Sasha and the Wedding Binary Search Tree

题意:

Sasha克服了重重困难和艰辛,终于决定与女友结婚。为此,他需要送她一枚订婚戒指。然而,他的女朋友并不喜欢这种浪漫的举动,但她喜欢二叉搜索树 † ^{\dagger} 。于是,Sasha决定送给她这样一棵树。

在程序员婚礼网站上花了大量时间后,他找到了一棵完美的二叉搜索树,树根位于顶点 1 1 1 。在这棵树上,顶点 v v v 的值等于 v a l v val_v valv

但一段时间后,他忘记了一些顶点的值。为了记住找到的树,Sasha想知道,如果已知所有顶点的值都是 [ 1 , C ] [1, C] [1,C] 段中的整数,他可以在网站上找到多少棵二叉搜索树。因为这个数字可能非常大,所以输出它的模数 998   244   353 998\,244\,353 998244353

† ^{\dagger} 二叉搜索树是一棵有根的二叉树,其中任意顶点 x x x 都具有以下性质:顶点 x x x 左子树中所有顶点的值(如果存在)都小于或等于顶点 x x x 的值,顶点 x x x 右子树中所有顶点的值(如果存在)都大于或等于顶点 x x x 的值。

题意:

二叉搜索树(BST,Binary Search Tree)重要性质之一:二叉搜索树的中序遍历每个节点的值是有序的。

那么我们可以对这个二叉搜索树进行中序遍历,把它的DFS序记录为一个数组,这个数组的元素和二叉搜索树的节点就能一一对应起来了。这个数组就应该是有序的。我们现在的任务就变成了保持有序的前提下 对-1的位置进行填数,问有多少种可能。

对一段连续的 − 1 -1 1 区间,它左边的数组元素值就是区间填数的下界,它右边的数组元素值就是区间填数的上界,特别的,当左边没有元素了,也就是左边是 a 0 a_0 a0,下界是 1 1 1,右边没有元素了,也就是右边是 a n + 1 a_{n+1} an+1,下界是 C C C。现在假设这段连续的 − 1 -1 1 区间的左端点为 l l l,右端点为 r r r,那么就有 p = r − l + 1 p=r-l+1 p=rl+1 个位置,值域是 [ a [ l − 1 ] , a [ r + 1 ] ] [a[l-1],a[r+1]] [a[l1],a[r+1]],可以填 q = a [ r + 1 ] − a [ l − 1 ] + 1 q=a[r+1]-a[l-1]+1 q=a[r+1]a[l1]+1 种数。

现在可以把问题转化为:把 p p p 个位置分给 q q q 种数的方法数,分好之后我们就可以按顺序填入,这样就是一个可行解。位置名额互相之间是相同的,不同数之间是不同的,这是个同球不同盒可以空盒的球盒问题。使用隔板法计算,答案数就是 C p + q − 1 p = C p + q − 1 q − 1 C_{p+q-1}^{p}=C_{p+q-1}^{q-1} Cp+q1p=Cp+q1q1

这里的G题讲思路之前详细讲了 隔板法,这里不多赘述。

在书写的时候会发现一个问题,那就是 n ≤ 5 ∗ 1 0 5 , C ≤ 1 0 9 n\le 5*10^5,C\le 10^9 n5105,C109,这导致 p ≤ 5 ∗ 1 0 5 , q ≤ 1 0 9 p\le 5*10^5,q\le 10^9 p5105,q109,预处理阶乘然后使用组合数定义式来计算的话,空间和时间都不允许。这时候需要对式子进行一步转化: C p + q − 1 p = ( p + q − 1 ) ! p ! ∗ ( q − 1 ) ! = q ∗ ( q + 1 ) ∗ ⋯ ∗ ( p + q − 1 ) p ! C_{p+q-1}^{p}=\dfrac{(p+q-1)!}{p!*(q-1)!}=\dfrac{q*(q+1)*\dots*(p+q-1)}{p!} Cp+q1p=p!(q1)!(p+q1)!=p!q(q+1)(p+q1)
这里分母 p p p 的阶乘是 5 ∗ 1 0 5 5*10^5 5105 以内的,可以直接预处理。分子是 p p p 个式子相乘,可以在 5 ∗ 1 0 5 5*10^5 5105 次内计算得到。这样一次计算组合数最多就是 p = 5 ∗ 1 0 5 p=5*10^5 p=5105 量级的。而在一个 n n n 中, ∑ p ≤ n \sum p\le n pn ,而且 ∑ n ≤ 5 ∗ 1 0 5 \sum n\le 5*10^5 n5105 。所以组合数总的运算次数就不会超过 5 ∗ 1 0 5 5*10^5 5105 了。

这种 C n + m n C_{n+m}^n Cn+mn 中,n很大很大 的组合数计算的时候经常采用这种方式来计算。

code:

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn=5e5+5;
//const int maxm=1e7+5;
const ll mod=998244353;

int T,n,c;

struct node{
	int lc,rc,val;
}nd[maxn];

int a[maxn],counter;
void dfs(int u){//中序遍历 
	if(u==-1)return;
	dfs(nd[u].lc);
	a[++counter]=nd[u].val;
	dfs(nd[u].rc);
}

ll fac[maxn];
ll qpow(ll a,ll b){
	ll ans=1,base=a;
	while(b){
		if(b&1){
			ans*=base;
			ans%=mod;
		}
		base*=base;
		base%=mod;
		b>>=1;
	}
	return ans;
}
ll inv(ll x){return qpow(x,mod-2);}
ll C(ll a,ll b){//C_a^b 
	if(b>a || b<0)return 0;
	ll fz=1;
	for(int i=a-b+1;i<=a;i++)
		fz=fz*i%mod;
	return fz*inv(fac[b])%mod;
}

int main(){
	fac[0]=fac[1]=1;
	for(int i=2;i<maxn;i++)
		fac[i]=fac[i-1]*i%mod;
	
	scanf("%d",&T);
	while(T--){
		scanf("%d%d",&n,&c);
		for(int i=1;i<=n;i++)
			scanf("%d%d%d",&nd[i].lc,&nd[i].rc,&nd[i].val);
		
		counter=0;
		dfs(1);
		
		ll ans=1;
		for(int i=1,p=0,l=1,r,q;i<=n;i++){
			if(a[i]==-1)p++;
			
			if(a[i]!=-1 || i==n){
				r=(a[i]!=-1)?a[i]:c;//值域[l,r] 
				q=r-l+1;
				l=r;
				ans=ans*C(p+q-1,p)%mod;
				p=0;
			}
		}
		cout<<ans<<endl;;
	}
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/391419.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用Apache ECharts同时绘制多个统计图表

目录 1、介绍 2、相关知识 3、代码 4、效果 &#x1f343;作者介绍&#xff1a;双非本科大三网络工程专业在读&#xff0c;阿里云专家博主&#xff0c;专注于Java领域学习&#xff0c;擅长web应用开发、数据结构和算法&#xff0c;初步涉猎Python人工智能开发和前端开发。 …

Linux第59步_“buildroot”构建根文件系统第1步_生成rootfs.tar和rootfs.ext4以及通过nfs下载测试

学习安装“buildroot”&#xff0c;通过配置构建根文件系统&#xff0c;编译生成rootfs.tar和rootfs.ext4&#xff0c;以及通过nfs下载测试。 1、了解学习目的&#xff1a; 1)、获取“buildroot”安装包&#xff1b; 2)、使用“buildroot”构建根文件系统&#xff1b; 3)、…

【论文精读】Latent Diffusion

摘要 Diffusion models&#xff08;DMs&#xff09;被证明在复杂自然场景的高分辨率图像合成能力优于以往的GAN或autoregressive &#xff08;AR&#xff09;transformer。作为基于似然的模型&#xff0c;其没有GAN的模式崩溃和训练不稳定问题&#xff0c;通过参数共享&#xf…

五分钟搭建本地大数据集群

引言 刚接触大数据以及部分接触大数据多年的伙伴可能从来没有自己搭建过一套属于自己的大数据集群&#xff0c;今天就花点时间聊聊怎么快速搭建一套属于自己、且可用于操作、调试的大数据集群 正文 本次搭建的组件都有以下服务以及对应的版本 hadoop&#xff08;3.2.4&…

人工智能学习与实训笔记(四):神经网络之NLP基础—词向量

人工智能专栏文章汇总&#xff1a;人工智能学习专栏文章汇总-CSDN博客 本篇目录 四、自然语言处理 4.1 词向量 (Word Embedding) 4.1.1 词向量的生成过程 4.1.2 word2vec介绍 4.1.3 word2vec&#xff1a;skip-gram算法的实现 4.2 句向量 - 情感分析 4.2.1 LSTM (Long S…

嵌入式Qt Qt中的字符串类

一.Qt中的字符串类 QString vs string&#xff1a; QString在Qt库中几乎是无所不在的 所有的Qt图形用户组件都依赖于QString 实验1 &#xff1a;QString 初体验 #include <QDebug> void Sample_1() {QString s "add";s.append(" "); // &q…

为什么MySQL不建议使用TEXT字段?

当我们深入探讨“为什么MySQL不建议使用TEXT字段&#xff1f;”这一问题时&#xff0c;可以从一下多个方面来详细理解这个问题&#xff1a; 1. 性能问题 性能问题是MySQL不建议使用TEXT字段的一个重要原因。TEXT字段通常以外部存储方式保存&#xff0c;而不是像固定长度或可变…

边缘计算第二版施巍松——第8章边缘计算系统实例

8.1边缘计算系统概述 1.Cloudlet 架构&#xff1a;移动设备-Cloudlet-云 cloudlet也可以像云一样为用户提供服务&#xff0c;Cloudlet离移动设备只有一跳的距离&#xff0c;具有物理距离的临近性&#xff0c;可以保证实时反馈时延低&#xff0c;又可以利用局域网的高带宽优势&…

【web | CTF】BUUCTF [BJDCTF2020]Easy MD5

天命&#xff1a;好像也挺实用的题目&#xff0c;也是比较经典吧 天命&#xff1a;把php的MD5漏洞都玩了一遍 第一关&#xff1a;MD5绕过 先声明一下&#xff1a;这题的MD5是php&#xff0c;不是mysql的MD5&#xff0c;把我搞迷糊了 一进来题目啥也没有&#xff0c;那么就要看…

计算机设计大赛 深度学习中文汉字识别

文章目录 0 前言1 数据集合2 网络构建3 模型训练4 模型性能评估5 文字预测6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习中文汉字识别 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xf…

[leetcode刷题] 组合

对于递归回溯我觉得是需要多写多分析&#xff0c;递归三部曲&#xff1a;1.返回值和参数&#xff1b;2.终止条件&#xff1b;3.单层递归逻辑 1.通常情况下返回值都是void&#xff0c;参数的话根据实际需求设计&#xff0c;如果设置了全局变量那输入参数就可以少写几个&#xf…

PyTorch – 逻辑回归

data 首先导入torch里面专门做图形处理的一个库&#xff0c;torchvision&#xff0c;根据官方安装指南&#xff0c;你在安装pytorch的时候torchvision也会安装。 我们需要使用的是torchvision.transforms和torchvision.datasets以及torch.utils.data.DataLoader 首先DataLoa…

【plt.imshow显示图像】:从入门到精通,只需一篇文章!【Matplotlib】

【plt.imshow显示图像】&#xff1a;从入门到精通&#xff0c;只需一篇文章&#xff01;【Matplotlib】 &#x1f680; 利用Matplotlib进行数据可视化示例 &#x1f335;文章目录&#x1f335; &#x1f4d8; 1. plt.imshow入门&#xff1a;认识并安装Matplotlib库&#x1f308…

Java编程在工资信息管理中的最佳实践

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

Crypto-RSA1

题目&#xff1a; 已知p,q,dp,dq,c求明文&#xff1a; 首先有如下公式&#xff1a; dp≡d mod (p-1)&#xff0c;dq≡d mod (q-1) &#xff0c; m≡c^d(mod n) &#xff0c; npq python代码解题如下&#xff1a; import libnump 863763376725700856709965348654109117132049…

浅谈语义分割、图像分类与目标检测中的TP、TN、FP、FN

语义分割 TP&#xff1a;正确地预测出了正类&#xff0c;即原本是正类&#xff0c;识别的也是正类 TN&#xff1a;正确地预测出了负类&#xff0c;即原本是负类&#xff0c;识别的也是负类 FP&#xff1a;错误地预测为了正类&#xff0c;即原本是负类&#xff0c;识别的是正类…

建造者模式-Builder Pattern

原文地址:https://jaune162.blog/design-pattern/builder-pattern/ 引言 现在一般大型的业务系统中的消息通知的形式都会有多种,比如短信、站内信、钉钉通知、邮箱等形式。虽然信息内容相同,但是展现形式缺不同。如短信使用的是纯文本的形式,钉钉使用的一般是Markdown的形…

挖掘在线零售数据:基于RFM理论的用户细分分析与营销策略

挖掘在线零售数据&#xff1a;基于RFM理论的用户细分分析与营销策略 基于RFM理论的用户细分分析项目背景和意义数据准备和预处理RFM分析1. 计算RFM指标2. 数据转换和处理 K-Means聚类分析结果和建议总结 基于RFM理论的用户细分分析 在商业运营中&#xff0c;了解客户并将其分组…

使用WGCLOUD监测摄像头的运行状态

WGCLOUD WGCLOUD是一款开源运维工具&#xff0c;免费高效&#xff0c;可以用来监测摄像头的工作状态&#xff0c;如果发现故障&#xff0c;那么WGCLOUD会发送告警通知消息&#xff0c;提醒我们的工程师进行处理 我们可以用WGCLOUD的PING监测模块&#xff0c;或者端口监测模块…

【开源】JAVA+Vue.js实现大学计算机课程管理平台

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 实验课程档案模块2.2 实验资源模块2.3 学生实验模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 实验课程档案表3.2.2 实验资源表3.2.3 学生实验表 四、系统展示五、核心代码5.1 一键生成实验5.2 提交实验5.3 批阅实…