这场还是很有含金量的,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}) (ai−ai−1) 的和。
请帮助Sasha,告诉他,如果他能以任何方式重新排列数组 a a a 中的元素,他能得到的最大数组美丽值是多少。
思路:
其实跟差分数组的思想差不多,推一下式子就能发现,这个美丽值其实就是 a n − a 1 a_n - a_1 an−a1,所以把最大数放在位置 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 4n−2 条对角线。
帮助小萨沙让女孩爱上他,并告诉他需要涂色的最少格子数。
思路:
这个题主要是得找张纸划拉划拉,推结论。
我们假设有个
3
∗
3
3*3
3∗3 的矩形,然后把格子缩成点,先看从左上到右下的所有对角线,如图。
再顺时针旋转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 2∗n−1 条,选取了 2 ∗ n − 1 − 1 2*n-1-1 2∗n−1−1 个点,每个点占两个对角线,这时就拿到了 2 ∗ ( 2 ∗ n − 1 − 1 ) = 4 ∗ n − 4 2*(2*n-1-1)=4*n-4 2∗(2∗n−1−1)=4∗n−4 条对角线,还差最后两条没有选,差的就是这两条,这两条要选点的话,选一个点只能产生一次贡献。
所以分类讨论,如果 k ≤ 4 ∗ n − 4 k\le 4*n-4 k≤4∗n−4 ,答案就是 ⌈ 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 4∗n−4<k≤4∗n−2,答案就是 2 ∗ n − 2 + ( k − 4 ∗ n + 4 ) 2*n-2+(k-4*n+4) 2∗n−2+(k−4∗n+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=4∗n−2 时答案会比 ⌈ 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 y⋅k 个硬币(即他的硬币数量将增加 y ⋅ ( k − 1 ) y \cdot (k - 1) y⋅(k−1) )。如果输了,他将输掉全部赌注( 即他的硬币数量将减少 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 的儿子节点,考虑如何转移。
- 对 d p [ u ] [ 0 ] dp[u][0] dp[u][0],肯定儿子节点的子树的所有点都不能包含危险点,这个点本身也不能是危险点,答案数就是 d p [ u ] [ 0 ] = 1 dp[u][0]=1 dp[u][0]=1。
- 对
d
p
[
u
]
[
1
]
dp[u][1]
dp[u][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。
- 这个点 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。
- 对
d
p
[
u
]
[
2
]
dp[u][2]
dp[u][2],两种情况:
- 这个点 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]。
- 这个点 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} 2k≤220。估计是暗示咱们去状压这个东西。具体来说,对每个边,用一个二进制数的第 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 2∗i−1 和 2 ∗ i 2*i 2∗i 个位置上。
用一个数组来存储每个边的路径信息。因为重复物品没有用,而且边最多可以有 1 0 5 − 1 10^5-1 105−1,状态最多有 2 20 ≈ 1 0 6 2^{20}\approx10^6 220≈106 种,直接跑背包DP是 O ( 1 0 5 ∗ 1 0 6 ) O(10^5*10^6) O(105∗106) 的,时间会爆。所以对物品进行去重,之后对剩下的物品跑01背包就行了。就做完了。
另外说点:为什么去重之后时间复杂度就达标了?去重之后物品能剩下几个?我也感觉成谜,这里给出我的一点想法,如果有大佬会算欢迎指点。
假设树上已经有了 x x x 条路径,这里我们再向上添加一条路径,使得不同的边(不同指的是属于路径不同,就是上面说的那个二进制数不同)变化量最大。
首先如果要变化量最大,显然 x x x 条路径应该都经过一条边,然后我们新增的这个路径也经过这个边。在这个边的左边,新增的路径沿边向下延伸,因为在经过一个点之后,先前的 x x x 个路径一定会分出去1股或多股,要变化量最大,一次只分出去1股,这样左边最多会有 x x x 次就会把原来的路径分出去,只剩下新增路径,左边就有了 x x x 条不同的边。右边可以使用另一种分股方式,由于最后只剩下新增路径的和左边重复了,所以有 x − 1 x-1 x−1 种不同的边,总的就是最多 2 ∗ x 2*x 2∗x 条不同的边。
因此上面 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(40∗220≈4∗107) 的复杂度。代码跑了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=r−l+1 个位置,值域是 [ a [ l − 1 ] , a [ r + 1 ] ] [a[l-1],a[r+1]] [a[l−1],a[r+1]],可以填 q = a [ r + 1 ] − a [ l − 1 ] + 1 q=a[r+1]-a[l-1]+1 q=a[r+1]−a[l−1]+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+q−1p=Cp+q−1q−1。
这里的G题讲思路之前详细讲了 隔板法,这里不多赘述。
在书写的时候会发现一个问题,那就是
n
≤
5
∗
1
0
5
,
C
≤
1
0
9
n\le 5*10^5,C\le 10^9
n≤5∗105,C≤109,这导致
p
≤
5
∗
1
0
5
,
q
≤
1
0
9
p\le 5*10^5,q\le 10^9
p≤5∗105,q≤109,预处理阶乘然后使用组合数定义式来计算的话,空间和时间都不允许。这时候需要对式子进行一步转化:
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+q−1p=p!∗(q−1)!(p+q−1)!=p!q∗(q+1)∗⋯∗(p+q−1)
这里分母
p
p
p 的阶乘是
5
∗
1
0
5
5*10^5
5∗105 以内的,可以直接预处理。分子是
p
p
p 个式子相乘,可以在
5
∗
1
0
5
5*10^5
5∗105 次内计算得到。这样一次计算组合数最多就是
p
=
5
∗
1
0
5
p=5*10^5
p=5∗105 量级的。而在一个
n
n
n 中,
∑
p
≤
n
\sum p\le n
∑p≤n ,而且
∑
n
≤
5
∗
1
0
5
\sum n\le 5*10^5
∑n≤5∗105 。所以组合数总的运算次数就不会超过
5
∗
1
0
5
5*10^5
5∗105 了。
这种 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;
}