题目
题目描述
有一个
n
n
n 个点的有向图,边都是编号小的指向编号大的。每个点有颜色,黑或白。一条合法的路径满足经过的点的颜色是黑白交错的。一个图是合法的,当且仅当合法路径数量为奇数。
现在,这个图的边和一些点的颜色都模糊不清了。你需要计算所有合法图的数量。即:计算所有合法的图的数量,要求其满足,一些特定点的颜色是指定的。
数据范围与提示
n
≤
1
0
5
n\le 10^5
n≤105 。提示:本题并不难。
思路
一种很自然的思路是,从后往前 d p \tt dp dp,因为前面的点先确定,后面的点在连边的时候,会对前面的点造成深远的影响,这不利于计算。
我们都会直接计算路径数量,形如
f
(
i
)
=
1
+
∑
⟨
i
,
j
⟩
∈
E
c
i
≠
c
j
f
(
j
)
f(i)=1+\sum_{\lang i,j\rang\in\Bbb E}^{c_i\ne c_j}f(j)
f(i)=1+⟨i,j⟩∈E∑ci=cjf(j)
由于我们只在乎奇偶性,容易想到,对于
j
(
i
<
j
)
j\;(i<j)
j(i<j),如果
f
(
j
)
f(j)
f(j) 是偶数,那么它不会造成任何作用。用
b
,
w
b,w
b,w 分别代表黑色点、白色点中
f
f
f 为奇数的数量,转移时枚举连了几条边,转移方程形如
∑
x
≤
b
2
∣
x
(
b
x
)
g
i
(
b
,
w
)
⋅
2
i
−
b
\sum_{x\le b}^{2|x}{b\choose x}g_{i}(b,w)\cdot 2^{i-b}
x≤b∑2∣x(xb)gi(b,w)⋅2i−b
可能是 2 ∣ x 2|x 2∣x,也可能是 2 ∤ x 2\nmid x 2∤x 。然后你发现,这个转移系数其实就是 2 b − 1 2^{b-1} 2b−1 嘛。再跟 2 i − b 2^{i-b} 2i−b 合在一起,就成了 2 i − 1 2^{i-1} 2i−1,与 b b b 无关了!
只需要存储 b b b 是否等于 0 0 0,因为 b = 0 b=0 b=0 时转移系数是 0 0 0 和 1 1 1,不是 2 b − 1 2^{b-1} 2b−1 。
复杂度 O ( 8 n ) \mathcal O(8n) O(8n) 。
代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
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;
}
const int Mod = 998244353;
const int MaxN = 200005;
int dp[MaxN][2][2][2];
int powtwo[MaxN], a[MaxN];
void addBy(int &x,int y){
x = (x+y)%Mod;
}
int main(){
int n = readint(); powtwo[0] = 1;
for(int i=1; i<=n; ++i){
a[i] = readint();
powtwo[i] = (powtwo[i-1]<<1)%Mod;
}
dp[0][0][0][0] = 1;
for(int i=1; i<=n; ++i)
for(int w=0; w<2; ++w)
for(int b=0; b<2; ++b)
for(int p=0; p<2; ++p){
if(a[n+1-i] != 0){ // choose black
if(w == 0) // no white, this = b1
addBy(dp[i][w][b|1][p^1],1ll*
powtwo[i-1]*dp[i-1][w][b][p]%Mod);
else if(i != 1){
int t = 1ll*powtwo[i-2]*dp[i-1][w][b][p]%Mod;
/* case 1: choose even, this = b1 */ ;
addBy(dp[i][w][b|1][p^1],t);
/* case 2: choose odd, this = b0 */ ;
addBy(dp[i][w][b][p],t);
}
}
if(a[n+1-i] != 1){ // choose white
if(b == 0) // no black, this = w1
addBy(dp[i][w|1][b][p^1],1ll*
powtwo[i-1]*dp[i-1][w][b][p]%Mod);
else if(i != 1){
int t = 1ll*powtwo[i-2]*dp[i-1][w][b][p]%Mod;
/* case 1: choose even, this = w1 */ ;
addBy(dp[i][w|1][b][p^1],t);
/* case 2: choose odd, this = w0 */ ;
addBy(dp[i][w][b][p],t);
}
}
}
int ans = 0;
for(int w=0; w<2; ++w)
for(int b=0; b<2; ++b)
addBy(ans,dp[n][w][b][1]);
printf("%d\n",ans);
return 0;
}
吐槽
这道题并不难啊,不知道为什么,大家做的不是很好。明明 n 4 ∼ n 5 n^4\sim n^5 n4∼n5 的 d p \tt dp dp 很好想,写出来之后也很容易优化啊……
另外讲讲 T i w - A i r - O A O \sf Tiw\text-Air\text-OAO Tiw-Air-OAO 的真巨佬做法(这个做法可以用来改编这道题,使它成为真毒瘤):
不妨设第一个点的颜色是黑色。如果后面有一个白色的点,满足其 f f f 为奇数,那么考虑将图 异或 这条边(有则删去,无则加入),显然第一个点的 f f f 的奇偶性将会翻转,从而整张图的路径数量奇偶性翻转。
这就是说,大多数 v a l i d g r a p h \rm valid\;graph validgraph 和 i n v a l i d g r a p h \rm invalid\;graph invalidgraph 是 一一对应 的1(请允许我穿插一下英文,全部使用中文有点繁杂)。而我们知道二者的和——就是所有 p o s s i b l e g r a p h \rm possible\;graph possiblegraph 。那么我们只要特殊处理一下,不对应的图就可以了。
由于最后一个点的 f f f 必然是奇数,所以只有它的颜色和第一个点的颜色相同时,才可能不存在对应关系。不妨设二者都是黑色。中间的白色点的 f f f 都是偶数,那么无论黑色点怎么连,都不产生任何影响,进而 黑色点的 f f f 都是奇数。那么每个白色点只需要连奇数个黑色点就行了。由于最后一个点是黑色的,所以每个白色点都有 1 2 \frac{1}{2} 21 的情况数合法(道理与 d p \tt dp dp 做法中的那个相同),并且两两独立。
然后这张图究竟是 v a l i d g r a p h \rm valid\;graph validgraph 还是 i n v a l i d g r a p h \rm invalid\;graph invalidgraph 呢?(二者要区分,因为前者需要加上,后者需要减去。)明显只跟黑色点数量有关嘛。是奇数则合法,否则不合法。
于是我们很轻松的写出了这个式子(其中
m
m
m 是边数,
i
i
i 是枚举
?
?
? 中有多少个黑色):
2
m
⋅
∑
i
≤
c
?
(
−
1
)
c
b
+
1
+
i
(
c
?
i
)
(
1
2
)
c
w
+
c
?
−
i
=
2
m
−
c
w
−
c
?
(
−
1
)
c
b
+
1
∑
i
≤
c
?
(
−
1
)
i
(
c
?
i
)
⋅
2
i
=
(
−
1
)
c
b
+
1
+
c
?
⋅
2
m
−
c
w
−
c
?
\begin{aligned} &2^{m}\cdot \sum_{i\le c_?}(-1)^{c_b+1+i}{c_?\choose i}\left({1\over 2}\right)^{c_w+c_?-i}\\ =\;&2^{m-c_w-c_?}(-1)^{c_b+1}\sum_{i\le c_?}(-1)^{i}{c_?\choose i}\cdot 2^i\\ =\;&(-1)^{c_b+1+c_?}\cdot 2^{m-c_w-c_?} \end{aligned}
==2m⋅i≤c?∑(−1)cb+1+i(ic?)(21)cw+c?−i2m−cw−c?(−1)cb+1i≤c?∑(−1)i(ic?)⋅2i(−1)cb+1+c?⋅2m−cw−c?
没错,真的化简成为了二项式展开的形式。怎么会这么巧妙!下面是他现场 A C AC AC 的代码,我添加了一点注释。
#include <bits/stdc++.h>
typedef long long ll;
const int N = 200000;
const int P = 998244353;
int norm(int x) {if( x >= P ) x -= P; return x;}
int reduce(int x) {if( x < 0 ) x += P; return x;}
void add(int &x, int y) {if( (x += y) >= P ) x -= P;}
void sub(int &x, int y) {if( (x -= y) < 0 ) x += P;}
int mpow(int b, int p) {
int r = 1; for(; p; p >>= 1, b = (ll)b * b % P)
if( p & 1 ) r = (ll)r * b % P;
return r;
}
int a[N + 5], c[3], n;
int solve() {
/* 让 c[0] 是与 1 同色的颜色 */ ;
if( a[1] == 1 ) std::swap(c[0], c[1]);
/* 大力计算! */ ;
int ret = mpow(2, ((ll)n * (n - 1) / 2 - 1 - c[2] - c[1]) % (P - 1));
if( (c[0] + c[2] + 1) & 1 ) ret = norm(P - ret);
/* 消除影响 */ ;
if( a[1] == 1 ) std::swap(c[0], c[1]);
return ret;
}
int main() {
scanf("%d", &n);
for(int i=1;i<=n;i++) {
scanf("%d", &a[i]);
c[a[i] == -1 ? 2 : a[i]]++;
}
if( n == 1 ) {
printf("%d\n", a[1] == -1 ? 2 : 1);
} else {
int ans = mpow(2, ((ll)n * (n - 1) / 2 - 1 + c[2]) % (P - 1));
if( a[1] == -1 ) {
c[2]--; // 多统计的值,我们将在下面确定其值
if( a[n] == -1 ) {
c[2]--; // 同理
/* 我们只计算首尾同色情况 */ ;
a[1] = a[n] = 0, add(ans, solve());
a[1] = a[n] = 1, add(ans, solve());
} else {
/* 让 1 去适应 n */ ;
c[a[n]]--;
a[1] = a[n], add(ans, solve());
}
} else {
/* 反正就是大力讨论,想办法让 1,n 同色 */ ;
c[a[1]]--;
if( a[n] == -1 ) {
c[2]--;
a[n] = a[1], add(ans, solve());
} else if( a[1] == a[n] ) {
c[a[n]]--;
add(ans, solve());
}
}
printf("%d\n", ans);
}
}
在他的这种做法下,可以改编出这样的题目:
- 有
T
T
T 次操作,每次交换两个
a
a
a 值,仍然求答案。
T
≤
1
0
5
T\le 10^5
T≤105 。
直接套这个公式,不需要更新 c c c 值,则 O ( log n ) \mathcal O(\log n) O(logn) 算快速幂就是了。 - 已知序列中有
x
x
x 个黑色,
y
y
y 个白色,求所有这种序列的答案之和。共
1
0
5
10^5
105 个询问。
讨论一下 1 , n 1,n 1,n 的颜色,然后套公式,乘组合数罢了。复杂度 O ( T log n + n ) \mathcal O(T\log n+n) O(Tlogn+n) 。 - 已知序列是循环的,给出循环节与循环次数,求答案。循环次数高达
1
0
9
10^9
109,循环节长度高达
1
0
6
10^6
106 。
又是这个公式的裸题。直接算呗,有啥好说的。
把出题人吊打了!再膜一次, T i w - A i r - O A O {\sf Tiw\text-Air\text-OAO} Tiw-Air-OAO,