模板:
bool is_prime(int x)
{
if (x < 2) return false;
for (int i = 2; i <= x / i; i ++ ) // 注意循环条件
if (x % i == 0)
return false;
return true;
}
说明:
i < n
和i <= n / 2
时间复杂度都是 O ( n ) O(n) O(n),过高i * i <= n
虽然时间复杂度是 O ( n 1 2 ) O(n^{\frac{1}{2}}) O(n21),但i * i
可能会溢出i <= n / i
,时间复杂度是 O ( n 1 2 ) O(n^{\frac{1}{2}}) O(n21)模板:
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ ) // 注意循环条件
if (x % i == 0)
{
int cnt = 0;
while (x % i == 0) x /= i, cnt ++ ;
cout << i << ' ' << cnt << endl;
}
if (x > 1) cout << x << ' ' << 1 << endl; // 至多存在一个大于sqrt(n)的质因子
}
说明:
i <= n / i
条件模板:
int primes[N], cnt; // primes[]存储所有素数(静态链表)
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(){
for(int i = 2; i <= n; i++){
if (st[i]) continue;
if(is_prime[i]) primes[cnt++]=i; //把素数存起来
for(int j = i; j <= n; j += i) //不管是合数还是质数,都用来筛掉后面它的倍数
st[j]=true;
}
}
说明:
模板:
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) {
primes[cnt++] = i; // i是当前可用的最小质数,保存到primes中
for (int j = i + i; j <= n; j += i)
st[j] = true; // 素数的倍数一定不是素数
}
}
}
说明:
模板:
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true; // 用最小质因子去筛合数primes[j] * i
if (i % primes[j] == 0) break; // 若prime[j]是i的最小质因子,则prime[j+1] * i的最小质因子依旧是prime[j]
}
}
}
说明:
i % primes[j] != 0
时,说明此时遍历到的primes[j]
不是i
的质因子,那么只可能是此时的primes[j] < i
的最小质因子,所以primes[j] * i
的最小质因子就是primes[j]
i % primes[j] == 0
时,说明i
的最小质因子是primes[j]
,因此primes[j] * i
的最小质因子也就应该是prime[j]
,之后接着用st[primes[j+1] * i] = true
去筛合数时,就不是用最小质因子去更新了,因为i
有最小质因子primes[j] < primes[j+1]
,此时的primes[j+1]
不是primes[j+1] * i
的最小质因子,此时就应该退出循环,避免之后重复进行筛选模板:
vector<int> get_divisors(int x)
{
vector<int> res;
for (int i = 1; i <= x / i; i ++ )
if (x % i == 0)
{
res.push_back(i);
if (i != x / i) res.push_back(x / i);
}
sort(res.begin(), res.end());
return res;
}
说明:
1
~sqrt(x)
,每次找到约数时,把自身和另一个同时放入int
最大值才有大约1500
个约数 n = p 1 c 1 × p 2 c 2 ∗ × . . . × p k c k n = p_1^{c_1}\times p_2^{c_2} *\times ... \times p_k^{c_k} n=p1c1×p2c2∗×...×pkck的约数个数等于
( c 1 + 1 ) ( c 2 + 1 ) . . . ( c k + 1 ) (c_1+1)(c_2+1)...(c_k+1) (c1+1)(c2+1)...(ck+1)
模板:
unordered_map<int, int> primes; // 用哈希表保存质数的指数
// 质数分解
for (int i = 2; i <= x / i; i++)
while(x % i == 0) {
x /= i;
primes[i]++;
}
if (x > 1) primes[x]++;
// 约数个数定理
LL res = 1;
for (auto elem : primes) res = res * (elem.second + 1);
说明:
s u m = ∏ i = 1 k ∑ j = 0 c i p i j = ( p 1 0 + p 1 1 + . . . + p 1 c 1 ) × . . . × ( p k 0 + p k 1 + . . . + p k c k ) sum=\prod_{i=1}^k{\sum_{j=0}^{c_i}{p_{i}^{j}}}=(p_1^0 + p_1^1 + ... + p_1^{c_1})\times...\times(p_k^0 + p_k^1 + ... + p_k^{c_k}) sum=i=1∏kj=0∑cipij=(p10+p11+...+p1c1)×...×(pk0+pk1+...+pkck)
模板:
LL res = 1;
for (auto elem : primes) {
int p = elem.first, a = elem.second;
LL sum = 1;
while(a--) sum = sum * p + 1;
res *= sum;
}
说明:
p 0 + p 1 + p 2 + . . . + p n = . . . ( ( 1 × p + 1 ) × p + 1 ) . . . p^0+p^1+p^2+...+p^n=...((1 \times p + 1)\times p + 1)... p0+p1+p2+...+pn=...((1×p+1)×p+1)...,其中里边迭代n
次。因此可直接用结果进行迭代计算,而不用一个变量存储中间值
模板:
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
说明:
φ ( n ) \varphi (n) φ(n)表示 1 1 1~ n n n中与 n n n互质的数的个数。
φ ( n ) = n ( 1 − 1 p 1 ) ( 1 − 1 p 2 ) ⋯ ( 1 − 1 p k ) \varphi \left( n \right) =n\left( 1-\frac{1}{p_1} \right) \left( 1-\frac{1}{p_2} \right) \cdots \left( 1-\frac{1}{p_k} \right) φ(n)=n(1−p11)(1−p21)⋯(1−pk1)
其中 n = p 1 c 1 p 2 c 2 ⋯ p k c k n=p_{1}^{c_1}p_{2}^{c_2}\cdots p_{k}^{c_k} n=p1c1p2c2⋯pkck
模板:
int phi(int x)
{
int res = x;
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
{
res = res / i * (i - 1);
while (x % i == 0) x /= i;
}
if (x > 1) res = res / x * (x - 1);
return res;
}
说明:
res = res / n * (n-1)
避免结果溢出模板:
int primes[N], cnt; // primes[]存储所有素数
int euler[N]; // 存储每个数的欧拉函数
bool st[N]; // st[x]存储x是否被筛掉(合数标记)
void get_eulers(int n)
{
euler[1] = 1;
for (int i = 2; i <= n; i ++ )
{
if (!st[i])
{
// 质数
primes[cnt ++ ] = i;
euler[i] = i - 1;
}
for (int j = 0; primes[j] <= n / i; j ++ )
{
int t = primes[j] * i;
st[t] = true;
if (i % primes[j] == 0)
{
euler[t] = euler[i] * primes[j];
break;
}
euler[t] = euler[i] * (primes[j] - 1);
}
}
}
说明:
快速计算 a k m o d p a^k \mod p akmodp
模板:
typedef long long LL;
int qmi(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL) res * a % p; // 防止乘法溢出
a = (LL) a * a % p; // 防止乘法溢出
k >>= 1;
}
return res;
}
说明:
模板
// 求x, y,使得ax + by = gcd(a, b)
int exgcd(int a, int b, int &x, int &y)
{
if (!b)
{
x = 1; y = 0;
return a;
}
int d = exgcd(b, a % b, y, x); // 递归结束时,y = x',x = y'
y -= (a/b) * x; // y = x'-(a / b) * y' = y - (a / b) * x
return d;
}
// 详细版
// 求x, y,使得ax + by = gcd(a, b)
int exgcd(int a, int b, int &x, int &y)
{
if (!b)
{
x = 1; y = 0;
return a;
}
int x_new, y_new;
int res = exgcd(b, a % b, x_new, y_new);
x = y_new; // x = y'
y = x_new - (a/b) * x; // y = x' - (a / b) * y'
return res;
}
说明:
ex_gcd(a, b, x, y)
求得的x
不是最终x
,因为此时ex_gcd
求出的x
是满足的 a x + b y = gcd ( a , b ) ax+by=\text{gcd}(a,b) ax+by=gcd(a,b)的,而不是满足 a x + b y = d ax+by=d ax+by=d的,二者相差 d / gcd ( a , b ) d / \text{gcd}(a, b) d/gcd(a,b)倍证明
a x + b y = g c d ( a , b ) = g c d ( b , a m o d b ) = b x ′ + a m o d b y ′ = b x ′ + ( a − ⌊ a b ⌋ b ) y ′ = b x ′ + a y ′ − ⌊ a b ⌋ b y ′ = a y ′ + b ( x ′ − ⌊ a b ⌋ b ) \begin{aligned} ax+by&=\mathrm{gcd}\left( a,b \right) \\ &=gcd\left( b,a\mathrm{mod}b \right) \\ &=bx\prime+a\mathrm{mod}b\,\,y\prime \\ &=bx\prime+\left( a-\lfloor \frac{a}{b} \rfloor b \right) y\prime \\ &=bx\prime+ay\prime-\lfloor \frac{a}{b} \rfloor by\prime \\ &=ay\prime+b\left( x\prime-\lfloor \frac{a}{b} \rfloor b \right) \end{aligned} ax+by=gcd(a,b)=gcd(b,amodb)=bx′+amodby′=bx′+(a−⌊ba⌋b)y′=bx′+ay′−⌊ba⌋by′=ay′+b(x′−⌊ba⌋b)
对比 a a a和 b b b的系数得:
{ x = y ′ y = x ′ − ⌊ a b ⌋ y ′ \begin{cases} x=y\prime\\ y=x\prime-\lfloor \frac{a}{b} \rfloor y\prime\\ \end{cases} {x=y′y=x′−⌊ba⌋y′
因此可根据递归得到的 x ′ x\prime x′和 y ′ y\prime y′计算 x x x和 y y y
已知 m 1 , m 2 ⋯ m k m_1,m_2\cdots m_k m1,m2⋯mk互质,方程
{ x ≡ a 1 ( m o d m 1 ) x ≡ a 2 ( m o d m 2 ) ⋯ x ≡ a k ( m o d m k ) \left\{ \begin{array}{c} x\equiv a_1\left( \mathrm{mod} m_1 \right)\\ \begin{array}{l} x\equiv a_2\left( \mathrm{mod} m_2 \right)\\ \cdots\\ x\equiv a_k\left( \mathrm{mod} m_k \right)\\ \end{array}\\ \end{array} \right. ⎩⎪⎪⎨⎪⎪⎧x≡a1(modm1)x≡a2(modm2)⋯x≡ak(modmk)
的最小正整数解 x = a 1 M M 1 − 1 + a 2 M M 2 − 2 + ⋯ a k M M k − k x=a_1MM_1^{-1}+a_2MM_2^{-2}+\cdots a_kMM_k^{-k} x=a1MM1−1+a2MM2−2+⋯akMMk−k,其中 M = m 1 m 2 ⋯ m k M=m_1m_2\cdots m_k M=m1m2⋯mk, M i = M m i M_i=\frac{M}{m_i} Mi=miM, M i − 1 M_i^{-1} Mi−1表示 M i M_i Mi模 m i m_i mi的逆元,可通过 M i x ≡ 1 ( m o d m i ) M_ix\equiv1(\mod m_i) Mix≡1(modmi)求出
模板
typedef long long LL;
// 扩展欧几里得算法
int exgcd(int a, int b, int &x, int &y)
{
if (!b)
{
x = 1; y = 0;
return a;
}
int d = exgcd(b, a % b, y, x); // 递归结束时,y = x',x = y'
y -= (a/b) * x; // y = x'-(a / b) * y' = y - (a / b) * x
return d;
}
// 中国剩余定理
LL x = 0, m1, a1;
cin >> m1 >> a1;
for (int i = 0; i < n - 1; i ++ )
{
LL m2, a2;
cin >> m2 >> a2;
LL k1, k2;
LL d = exgcd(m1, -m2, k1, k2); // 求的不是最终解k1和k2
if ((a2 - a1) % d)
{
x = -1;
break; // 无解
}
k1 *= (a2 - a1) / d; // 变换成真正的解
int t = m2 / d; // k1 = k1 + k * m2 / d
k1 = (k1 % t + t ) % t; // 变换成最小的正整数(防止C++对负数模取余)
x = k1 * m1 + a1;
// 把两个方程合并成一个方程
LL m = abs(m1 / d * m2); // 变成正数
a1 = k1 * m1 + a1;
m1 = m;
}
if (x != -1) x = (x % m1 + m1) % m1; // 变成最小正整数(防止C++对负数模取余)
cout << x << endl;
说明
模板
// a[N][N+1]是增广矩阵
int gauss()
{
int c, r;
// 按列遍历,化成行阶梯矩阵
for (c = 0, r = 0; c < n; c ++)
{
// 找到当前列绝对值最大元素所在的行(搜索第r行~最后一行)
int t = r;
for (int i = r + 1; i < n; i ++ )
if (fabs(a[i][c]) > fabs(a[t][c]))
t = i;
if (fabs(a[t][c]) < eps) continue; // 当前列全是0
for (int j = c; j <= n; j ++ ) swap(a[t][j], a[r][j]); // 将绝对值最大的行换到最顶端
for (int j = n; j >= c; j -- ) a[r][j] /= a[r][c]; // 将当前上的首位变成1,注意从后往前遍历
for (int i = r + 1; i < n; i ++ ) // 用当前行将下面所有的列消成0
if (fabs(a[i][c]) > eps)
for (int j = n; j >= c; j -- )
a[i][j] -= a[r][j] * a[i][c]; // 注意从后往前删,否则出现写后读错误
r ++ ;
}
if (r < n)
{
for (int i = r; i < n; i ++ )
if (fabs(a[i][n]) > eps)
return 2; // 出现0!=0,无解
return 1; // 都是0=0,有无穷多组解
}
// 化成单位阵,增广矩阵的扩展部分为方程的解
for (int i = n - 1; i >= 0; i -- )
for (int j = i + 1; j < n; j ++ )
a[i][n] -= a[i][j] * a[j][n];
return 0; // 有唯一解
}
说明
C a b = C a − 1 b + C a − 1 b − 1 C_{a}^{b}=C_{a-1}^{b}+C_{a-1}^{b-1} Cab=Ca−1b+Ca−1b−1
模板
for (int i = 0; i < n; i ++ )
for (int j = 0; j <= i; j ++ )
if (!j) c[i][j] = 1;
else c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
说明
i=0
时,不会执行else
部分,因此不会出现数组越界假设 m m m是一个很大的质数
( n − 1 ) ! (n-1)! (n−1)!的模 m m m逆元是 ( ( n − 1 ) ! ) m − 2 ((n-1)!)^{m-2} ((n−1)!)m−2
n ! n! n!的模 m m m逆元是 ( n ! ) m − 2 (n!)^{m-2} (n!)m−2
则 infact ( n ) = infact ( n − 1 ) × ( n ! ) m − 2 ( ( n − 1 ) ! ) m − 2 = infact ( n − 1 ) × n m − 2 \text{infact}(n)=\text{infact}(n-1) \times \frac{(n!)^{m-2}}{((n-1)!)^{m-2}}=\text{infact}(n-1) \times n^{m-2} infact(n)=infact(n−1)×((n−1)!)m−2(n!)m−2=infact(n−1)×nm−2
其中 n m − 2 n^{m-2} nm−2可通过快速幂求得
模板
// 快速幂模板
int qmi(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
// 预处理阶乘的余数和阶乘逆元的余数
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i ++ )
{
fact[i] = (LL) fact[i - 1] * i % mod;
infact[i] = (LL) infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}
说明
long long
,然后再取模,否则结果会溢出若 p p p是质数,则对于任意整数 1 ≤ m ≤ n 1 \le m \le n 1≤m≤n,有:
C n m = C n m o d p m m o d p × C n ÷ p m ÷ p m o d p C_n^m=C_{n \mod p}^{m \mod p} \times C_{n \div p}^{m \div p} \mod p Cnm=Cnmodpmmodp×Cn÷pm÷pmodp
模板
// 快速幂模板
int qmi(int a, int k)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
// 通过定理求组合数C(a, b)
int C(int a, int b)
{
int res = 1;
for (int i = 1, j = a; i <= b; i ++, j -- )
{
res = (LL)res * j % p; // 构造分子a * (a - 1) * ... * (a - b + 1)
res = (LL)res * qmi(i, p - 2) % p; // 构造(b!)的逆元(b!)^{p-2}
}
return res;
}
int lucas(LL a, LL b)
{
if (a < p && b < p) return C(a, b);
return (LL)C(a % p, b % p) * lucas(a / p, b / p) % p;
}
说明
int
还是long long
模板
int primes[N], cnt; // 存储所有质数
int sum[N]; // 存储每个质数的次数
bool st[N]; // 存储每个数是否已被筛掉(合数标记)
// 线性筛法求素数
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
// 求n!质因数分解后,在质数p的次数
int get(int n, int p)
{
int res = 0;
while (n)
{
res += n / p;
n /= p;
}
return res;
}
// 高精度乘低精度模板
vector<int> mul(vector<int> a, int b)
{
vector<int> c;
int t = 0;
for (int i = 0; i < a.size(); i ++ )
{
t += a[i] * b;
c.push_back(t % 10);
t /= 10;
}
while (t)
{
c.push_back(t % 10);
t /= 10;
}
return c;
}
get_primes(a); // 预处理范围内的所有质数
for (int i = 0; i < cnt; i ++ ) // 求每个质因数的次数
{
int p = primes[i];
sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}
vector<int> res;
res.push_back(1);
for (int i = 0; i < cnt; i ++ ) // 用高精度乘法将所有质因子相乘
for (int j = 0; j < sum[i]; j ++ )
res = mul(res, primes[i]);
说明
get
方法计算)get(int n, int p)
求n!
质因数分解在 p p p的次数的数学公式$\lfloor \frac{n}{p} \rfloor +\lfloor \frac{n}{p^2} \rfloor +\lfloor \frac{n}{p^3} \rfloor +\cdots $catalan ( n ) = C 2 n n n + 1 \text{catalan}(n)=\frac{C_{2n}^n}{n+1} catalan(n)=n+1C2nn
∣ ⋃ k = 1 n S k ∣ = ∑ i ∣ S i ∣ − ∑ i , j ∣ S i ∩ S j ∣ + ∑ i , j , k ∣ S i ∩ S j ∩ S k ∣ − ⋯ \left| \bigcup_{k=1}^n{S_k} \right|=\sum_i{\left| S_i \right|}-\sum_{i,j}{\left| S_i\cap S_j \right|}+\sum_{i,j,k}{\left| S_i\cap S_j\cap S_k \right|}-\cdots ∣∣∣∣∣k=1⋃nSk∣∣∣∣∣=i∑∣Si∣−i,j∑∣Si∩Sj∣+i,j,k∑∣Si∩Sj∩Sk∣−⋯
C n 1 + C n 2 + C n 3 + ⋯ + C n n = 2 n − C n 0 = 2 n − 1 C_{n}^{1}+C_{n}^{2}+C_{n}^{3}+\cdots +C_{n}^{n}=2^n-C_{n}^{0}=2^n-1 Cn1+Cn2+Cn3+⋯+Cnn=2n−Cn0=2n−1
模板
int res = 0;
// 一共有2^m-1种集合
for (int i = 1; i < 1 << m; i ++ )
{
int t = 1, s = 0; // t表示当前集合的乘积,s表示符号(正负交替)
// 遍历每种集合
for (int j = 0; j < m; j ++ )
if (i >> j & 1)
{
// 元素存在
if ((LL)t * p[j] > n)
{
t = -1; // 构造的质数乘积比n大,舍弃
break;
}
t *= p[j];
s ++ ;
}
if (t != -1)
{
if (s % 2) res += n / t; // 根据s的奇偶性实现正负交替
else res -= n / t;
}
}
说明
2 m 2^m 2m可以用1 << m
实现
m个元素可以构造 2 m − 1 2^m-1 2m−1种不同的非空集合,集合包含的元素可用 m m m位二进制表示,例如5=101b
,表示集合有第1个元素和第3个元素
对于每个元素都有 C n 1 − C n 2 + C n 3 − ⋯ + ( − 1 ) k − 1 C n n = 1 C_{n}^{1}-C_{n}^{2}+C_{n}^{3}-\cdots +\left( -1 \right) ^{k-1}C_{n}^{n}=1 Cn1−Cn2+Cn3−⋯+(−1)k−1Cnn=1,即每个元素取到的次数都是1,不重复
1 1 1~ n n n中能被 p p p整除的数的个数为$\lfloor \frac{n}{p} \rfloor $
遍历的次序与公式不太一样,不是先加 m m m个数,再减去 C m 2 C_m^2 Cm2个数。实际上,当集合元素个数是奇数时,符号为正,反之为负。因此可根据集合个数的奇偶性判断符号的正负
模板是基于整除个数问题设计的
时间复杂度为 O ( 2 n ) O(2^n) O(2n)
公平组合游戏ICG
若一个游戏满足以下条件,则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。
给定N堆物品,第 i i i堆物品有 A i A_i Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。
定义
我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。
必胜状态,先手进行某一个操作,留给后手是一个必败状态时,对于先手来说是一个必胜状态。即先手可以走到某一个必败状态。
必败状态,先手无论如何操作,留给后手都是一个必胜状态时,对于先手来说是一个必败状态。即先手走不到任何一个必败状态。
定理
NIM博弈先手必胜,当且仅当 A 1 ⊕ A 2 ⊕ ⋯ ⊕ A n ≠ 0 A_1\oplus A_2\oplus \cdots \oplus A_n\ne 0 A1⊕A2⊕⋯⊕An=0
性质
性质证明
分析
约束条件
每次可拿石子的个数不是任意的,而是集合 S S S中的元素
概念
mex \text{mex} mex
mex(S) \text{mex(S)} mex(S)表示不属于集合 S S S的最小自然数,即$\text{mex}(S)=\min \left{ x \mid x\notin S\land x\in \mathrm{N} \right} $。
- 例如当 S = 1 , 2 S={1, 2} S=1,2时, mex ( S ) = 0 \text{mex}(S)=0 mex(S)=0
- 例如当 S = 0 , 2 S={0, 2} S=0,2时, mex ( S ) = 1 \text{mex}(S)=1 mex(S)=1
有向图游戏
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。
SG \text{SG} SG
在有向图游戏中,对于每个节点 x x x,设从 x x x出发共有 k k k条有向边,分别到达节点 y 1 , y 2 , ⋯ , y k y_1, y_2,\cdots, y_k y1,y2,⋯,yk,定义 SG ( x ) \text{SG}(x) SG(x)为 x x x的后继节点 y 1 , y 2 , ⋯ , y k y_1, y_2,\cdots, y_k y1,y2,⋯,yk的 SG \text{SG} SG函数值构成的集合再执行 mex ( S ) \text{mex}(S) mex(S)运算的结果,即:
SG ( x ) = mex ( SG ( y 1 ) , SG ( y 2 ) , ⋯ , SG ( y k ) ) \text{SG}(x) = \text{mex}({\text{SG}(y1), \text{SG}(y2),\cdots, \text{SG}(yk)}) SG(x)=mex(SG(y1),SG(y2),⋯,SG(yk))
特别地,整个有向图游戏 G G G的 SG \text{SG} SG函数值被定义为有向图游戏起点 s s s的 SG \text{SG} SG函数值,即 SG ( G ) = SG ( s ) \text{SG}(G) = \text{SG}(s) SG(G)=SG(s)。
有向图游戏的和
设 G 1 , G 2 , ⋯ , G m G_1, G_2, \cdots, G_m G1,G2,⋯,Gm是 m m m个有向图游戏。定义有向图游戏 G G G,它的行动规则是任选某个有向图游戏 G i G_i Gi,并在 G i G_i Gi上行动一步。 G G G被称为有向图游戏 G 1 , G 2 , ⋯ , G m G_1, G_2,\cdots, G_m G1,G2,⋯,Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
S G ( G ) = S G ( G 1 ) ⊕ S G ( G 2 ) ⊕ ⋯ ⊕ S G ( G m ) \mathrm{SG}\left( G \right) =\mathrm{SG}\left( G_1 \right) \oplus \mathrm{SG}\left( G_2 \right) \oplus \cdots \oplus \mathrm{SG}\left( G_m \right) SG(G)=SG(G1)⊕SG(G2)⊕⋯⊕SG(Gm)
性质
性质证明
类似NIM游戏的证明
定理
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于 0 0 0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于 0 0 0。
模板
int s[N], f[M];
memset(f, -1, sizeof f); // 初始化
// 记忆化搜索(备忘录法)
int sg(int x)
{
if (f[x] != -1) return f[x]; // 已经计算过
// 构造子树
unordered_set<int> S;
for (int i = 0; i < m; i ++ )
{
int sum = s[i];
if (x >= sum) S.insert(sg(x - sum)); // 保存后继结点到S中(递归)
}
// mem(x)
for (int i = 0; ; i ++ )
if (!S.count(i))
return f[x] = i;
}
// 把n堆石子看成n个独立的有向图G,把各个有向图结果做异或即可得到答案
int x, res = 0;
for (int i = 0; i < n; i ++ )
{
cin >> x;
res ^= sg(x);
}
P.S.
部分内容来自y总的模板
如果大家有兴趣,可以去Acwing《算法基础课》看看
我在Acwing也分享了一份,欢迎去围观