我们是时间的旅行者
我们是时间的旅行者 追逐时间的攀缘者qwq。
小时候最喜欢的一集。
线性代数
向量
我们定义向量是多维空间中一条带方向的线段,由于不太需要考虑其绝对位置关系,只考虑相对位置,一般都是平移到原点然后记录终点的坐标,记为 \(\vec x = (a_1, a_2, ..., a_n)\)。
一般来说我们只探讨二维向量,因为是比较容易想的。
比如说:

我们可以称这个向量为 \(u\),也可以表示为 \(\vec B\),显然 \(\vec B = (2, 3)\)。
同时在空间中也有一些特殊的向量,比如:
- \(\vec 0\),就是原点,由原点指向原点,初始的向量。
- \(\vec i\),在 \(x\) 轴方向前进一步的向量。
- \(\vec j\),在 \(y\) 轴方向前进一步的向量。
- \(\vec k, l, m ...\),在更高维度上前进一步的向量。
我们称这些向量为基向量。
如图:

向量 \(\vec i\) 和 \(\vec j\) 就是基向量。
下面介绍一下向量的加减法则:
- \(\vec a + \vec b = (x_a + x_b, y_a + y_b)\)。
- \(\vec a - \vec b = (x_a - x_b, y_a - y_b)\)。
如图:

\(\vec a + \vec b = \vec c\)。
如果是减法我们就加上其相反向量即可。
向量的数乘:
- \(p \vec a = (px_a, px_b)\)。
向量的点乘:
- \(\vec a \cdot \vec b = |a| \cdot |b| \cdot \cos <a, b>\)。
矩阵
\(n \times m\) 的矩阵。
\(n \times n\) 的矩阵称之为方阵。
运算:
- 矩阵的加法:按位相加即可。
- 矩阵的数乘:按位相乘即可。
- 矩阵的点乘:\(c_{i, j} = \sum a_{i, k} \times b_{k, j}\)。
- 矩阵的转置:写为 \(A^T\),\(A^T_{i, j} = A_{j, i}\)。
基础矩阵:
- 单位矩阵:即主对角线上全为 \(1\),乘上任何矩阵都为它本身。
- 零矩阵:全为 \(0\) 的矩阵,乘上任何矩阵都为零矩阵。
很多 DP 转移可以写成矩阵形式,由于矩阵乘法是有结合律的,所以可以快速幂做。
唯一有用的可能是高斯消元,那东西必须得看代码。
行列式
行列式仅针对方阵。
然后计算公式是:
一些性质:
- 交换两行,行列式值乘上 \(-1\)。
- 倍加,行列式值不变。
- 和的拆分(一行或一列),直接拆分相加即可。
- \(\det(AB) = \det(A) \det(B)\)
根据性质,可以用高斯消元把下面全部消成 \(0\),然后就只能取对角线上的值了。
LGV 引理
- Pro:给定一个具有特殊性质的平面图 DAG,求从初始点集到结束点集的不相交路径方案数(两个点集点的个数相同)。
特殊性质:将起始点集以及结束点集像二部图一样分开排列,将线拉直不相交(点集内部不连边,有的话就去除,反正无论如何也不会走,因为不相交)。
网格图是其中一种。
- 结论:定义 \(w(P)\) 为路径 \(P\) 上边的权的乘积(初始全为 \(1\)),\(f(s, t)\) 表示 \(s\) 到 \(t\) 可能的路径之和,\(A\) 为初始点集,\(B\) 为结束点集,那么:
即这个矩阵的行列式。
- 证明:咕咕咕。
Matrix-Tree 定理
- 
Pro1:给你一张无向图,求其生成树个数。 
- 
结论:令 \(D\) 为度数矩阵(主对角线为度数,带权),\(L\) 为邻接矩阵(可以有权),\(A = D - L\), 然后选择 \(A\) 中的一个格子,删掉它所在列和行,那么 \(ans = \det(A)\)。 
- 
证明:咕咕咕。 
- 
Pro2:给你一张有向图,求其外向/内向生成树个数。 
- 
结论,外向:\(ans = \det(A = D_{in} - L)\),内向:\(ans = \det(A = D_{out} - L)\)。 
- 
证明:咕咕咕。 
BEST 定理
最好的定理!!!
- 
Pro:给你一张有向图,要你求从 \(x\) 出发的欧拉回路条数。 
- 
结论:满足(\(D_x\) 为以 \(x\) 为根的内向生成树个数): 
- 证明:咕咕咕。
线性基
基可以理解为基向量,线性基通常是关于异或的基。
然后就是做高斯消元,矩阵中剩下的数就是线性基所要的数了,然后表示一下即可。
然后我们可以用线性基求所有数的最大异或值和最小异或值,直接贪心即可。
如果要求第 \(k\) 小/大异或值,那么可以重新调整一下基然后再像 0/1 Trie 一样搞,不过通常只会考前两个。
拉格朗日插值
即给出 \(n\) 个点能确定一个 \(n - 1\) 次多项式,那么此时可以通过以下公式求这个多项式的点值:
其实通常我们取连续值然后用阶乘计算,那么计算单个就变成了 \(O(n)\) 的了。
一般如果有式子那么就猜多项式系数,直接开到最大的做就完事了。。。
题目
P2044 [NOI2012] 随机数生成器
- Description:给定 \(X_0, m\) 和递推公式 \(X_{n + 1} = (aX_n + c) \mod m\),求 \(X_n\),\(1 \le n \le 10^18\)。
首先这个题我们肯定要用矩阵快速幂,考虑构造矩阵:
然后初始矩阵为:
然后答案就是 \(P^n \cdot Q\)。
P1962 斐波那契数列
- Description:求斐波那契数列第 \(n\) 项,\(1 \le n \le 10^9\)。
直接上矩阵,不多说。
P2447 [SDOI2010] 外星千足虫
- Description:给你若干个方程,形如一堆数的和 \(\bmod 2\) 的值,让你解方程,并推算出最早可以解出的时刻。
其实我们不难发现可以转化成异或方程组,然后我们可以高斯消元解出来,也可以用线性基求解。
但是题目太毒瘤了,我们需要用 bitset 优化一下常数。
P2886 [USACO07NOV] Cow Relays G
- Description:给你一张有向图,让你求从 \(S\) 到 \(T\) 恰好经过 \(K\) 条边的最短路。
我们定义广义矩阵乘法为 Floyd 的转移形式,然后我们把邻接矩阵对着这个东西求 \(K\) 次快速幂,最后 \((S, T)\) 的值就是答案了。
P1397 [NOI2013] 矩阵游戏
- Description:有些复杂,可以自己看原题。
其实就是每一行矩阵快速幂,然后换行的时候乘上另外一个矩阵,对 \(m\) 行都做一遍这个事情,发现也可以矩阵快速幂,简单来说就是把式子完全展开然后构造两个矩阵硬算就好了。
P3746 [六省联考 2017] 组合数问题
- Description:让你求 \(\sum_{i = 0}^{n} C_{nk}^{ik + r} \bmod p\)。
其实我们组合意义一下,就是在 \(nk\) 个不同物品中选 \(t\) 个,且满足 \(t \bmod k = r\) 的方案数有多少,构造矩阵 DP 即可。
P6573 [BalticOI 2017] Toll
- Description:有些复杂,可以自己看原题。
考虑到 \(k\) 最多为 \(5\),所以我们每 \(5\) 个构造一个与下 \(5\) 个的矩阵,然后求 \(s\) 到 \(t\) 的最短路就是直接把两个所在矩阵中间的矩阵全部乘起来(广义矩阵乘法:Floyd 更新方式),然后看对应位置。
考虑到矩阵乘法是有结合律的,所以我们多次询问采用线段树进行解决。
P3193 [HNOI2008] GT考试
- Description:有些复杂,可以自己看原题。
考虑设状态 \(f_{i, j}\) 为算到第 \(i\) 位,后 \(j\) 位与 \(t\) 匹配的方案数,这个时候我们需要求一个数组 \(g_{i, j}\),表示匹配了 \(i\) 位后再加一个字符能匹配 \(j\) 位的方案数,用 KMP 可以快速算出,然后对 DP 进行矩阵快速幂即可。
P7736 [NOI2021] 路径交点
- Description:有些复杂,可以自己看原题。
我们发现本质就是 LGV 引理里的容斥,利用 LGV 引理做,完事。
P3317 [SDOI2014] 重建
- Description:有些复杂,可以自己看原题。
我们发现 Matrix-Tree 定理本质上是要求生成树权乘积和的,那么我们考虑题目让我们求什么:
就是还要保证生成树以外的边不被选中。
然后共同除以 \(1 - p_e\),然后再乘上,变成:
P4336 [SHOI2016] 黑暗前的幻想乡
- Description:有些复杂,可以自己看原题。
我们发现容斥 + Matrix-Tree 定理即可。
就是 \(n - 1\) 家公司边的生成树 - \(n - 2\) + \(n - 3\)。
为什么要容斥呢?因为可能有多家公司用很多边,根据直觉,就应该这样容斥。
组合计数
很简单的一些公式:
- 递推公式:
- 吸收恒等式:
- 上指标求和(利用递推公式,加一项):
- 平行恒等式:
- 范德蒙德卷积:
- 二项式定理:
来看几个问题:
- 问题 \(1\):给你 \(n\) 个相同的球和 \(m\) 个不同的盒子,问有多少种办法把这 \(n\) 个球放进盒子?盒子不可以空。
利用插板法,不难得到 \(\binom{n - 1}{m - 1}\)。
- 问题 \(2\):给你 \(n\) 个相同的球和 \(m\) 个不同的盒子,问有多少种办法把这 \(n\) 个球放进盒子?盒子可以空。
我们借 \(m\) 个元素过来,就显然是 \(\binom{n + m - 1}{m - 1}\)。
- 问题 \(3\):有不定方程 \(X_1+X_2+···+X_n = S\),每个量都是非负整数,对于每个 \(X_i\), 有限制 \(X_i \le K\) ,计数这样的不定方程的解数。\(n, K \le 10^5\)。
考虑到容斥,所有的减去一个大于 \(K\),加上两个大于 \(K\),问题就把 \(S\) 减去若干个 \(K\) 就好了,但是需要注意有顺序,所以要乘上 \(\binom{n}{i}\)。
然后是格路计数,就仿照卡特兰数,画两条线,然后上面那根对折以下就好了。
然后需要注意的是如果是有上下界,直接容斥即可,不要犹豫。
然后是斯特林数(由于不会打所以直接括号表示):
- 第一类斯特林数:\((n, m)_1\) 表示 \(n\) 个不同元素构成 \(m\) 个圆排列方案。递推:
- 第二类斯特林数:\((n, m)_2\) 表示 \(n\) 个不同元素构成 \(m\) 个非空子集。递推:
需要特别特别特别记住的公式
利用这个公式我们可以推很多组合意义。
比如 Card 和自然数幂和(虽然不如直接拉插)。
题目
P1450 [HAOI2008] 硬币购物
- Description:给你 \(4\) 种硬币个数及面值,问恰好买 \(s\) 元物体有多少种方式?
考虑容斥,限制放的硬币种类,最开始再预处理一下多重背包就好了。
P1641 [SCOI2010] 生成字符串
- Description:\(n\) 个 \(0\) 和 \(m\) 个 \(1\),要求前 \(k\) 位 \(1\) 的个数不能少于 \(n\) 的个数,问方案总数。
考虑组合意义,就是网格图画线,然后翻折一下求方案个数,然后稍微容斥一下即可。
CF1278F Cards
- Description:有点复杂。
概率的题就是选中乘上选中的概率乘上没选中的乘上没选中的概率,推出来一下,然后用斯特林数幂展开,然后用二项式定理就做完了。
CF622F The Sum of the k-th Powers
- Description:求 \(\sum_{i = 1}^{n} i^k\)。
这个东西有个很明显的结论,就是它是 \(k + 1\) 次多项式,然后我们用拉格朗日插值插连续的 \(k + 2\) 个点值,算阶乘预处理就完了。
[AGC013E] Placing Squares
- Description:有点复杂。
考虑分两类点,一类点是标记点,一类是没有标记点,然后我们设置两个不同的矩阵转移 DP,由于一共只会有线性个标记点,所以我们只需要做 \(k - 1\) 次快速幂和 \(k\) 次乘法就完了。
CF1895F Fancy Arrays
- Description:有点复杂。
考虑容斥,容斥下线后发现 \(\le x - k + 1\) 的贡献就是 \((2k + 1)^{n - 1}\),然后 \(\le x\) 的就弄个矩阵优化 DP 就好了。
[ABC281G] Farthest City
- Description:有点复杂。
考虑我们将图分层做 DP,那么第 \(i\) 层的点只会连向第 \(i + 1\) 层或者第 \(i\) 层自己,记录一下层的数量的当前层的点数就好了,注意 \(n\) 点必须要在最后一层,所以要把剩余点数减 \(1\)。
数据结构
并查集
其实并查集的本质就是一颗树,只不过看你是只维护连通性还是维护其他信息。
种类并查集
就按照种类分层,然后不同种类之间互相连边,通常能够描述不同种类之间的矛盾关系。
带权并查集
就是树上每条边都带权,然后由于有边权,所以大部分情况下不能路径压缩,只能按秩合并。
笛卡尔树
考虑这样一个对于区间 \([l, r]\) 建树过程:
- 拎出最大值做根节点。
- 最大值两边的区间继续处理。
就是一个大根堆。
但是我们实际操作时不这么操作,因为复杂度是 \(O(n^2)\) 的。
我们这么处理:
for ( int i = 1; i <= n; i ++ ) {
        int tmp = cnt;
        while ( tmp && a[id[tmp]] > a[i] ) {
            tmp --;
        }
        if ( tmp ) {
            tree[id[tmp]].r = i;
        }
        if ( tmp < cnt ) {
            tree[i].l = id[tmp + 1];
        }
        id[++ tmp] = i, cnt = tmp;
    }
就每次试着往右边加,如果不行往上跳,复杂度 \(O(n)\)。
如果忘了也可以直接分治 + 线段树 \(O(n \log_2 n)\) 做,应该不会卡。
哦,还有就是笛卡尔树的启发式搜索复杂度是对的(每次都往小的区间遍历)。
左偏树
先建一个堆。
定义 \(dist_i\) 表示 \(i\) 节点向右最多能走的节点个数。
然后合并堆的时候不断往下合并,如果左儿子的 \(dist\) 小于了 右儿子,就交换。
为什么复杂度是对的,因为每一层都是满的(会交换),所以 \(dist\) 最多不超过 \(\log_2 n\)。
放个详细点的合并代码:
int merge(int x, int y){
	if(!x || !y){
		return x + y; 
	}
	if(a[y] < a[x]){
		swap(x, y);
	}
	rs[x] = merge(rs[x], y);
	if(dist[ls[x]] < dist[rs[x]]){
		swap(ls[x], rs[x]);
	}
	dist[x] = dist[rs[x]] + 1;
	return x;
}
可持久化
可持久化的本质就是维护多个历史版本,将上一个版本需修改的节点原封不动的复制下载,继续更改。
然后可持久化数据结构复杂度必须是非均摊的,否则可以通过单一元素不同版本多次查询卡你(如 Splay)。
然后复杂度正确必须保证新增节点个数正确与结构正确。
可持久化字典树
一般是 O/1 Trie。
就先可持久化以下。
然后我们会发现这个东西对于某些差分问题有可减性,即对应节点相减可以把重复部分消掉,然后我们可以进行最大/最小/第 \(k\) 大的异或值的求解。
放个插入代码,可能有点不一样:
void insert ( int &node, int k, int x ) { // k 表示位数
    tree[++ cnt] = tree[node];
    tree[cnt].siz ++;
    node = cnt;
    if ( k == -1 ) {
        return ;
    }
    insert ( tree[node].ch[( x >> k ) & 1], k - 1, x );
}
上述代码如果是第 \(i\) 个历史版本更改而来要把 \(rt_{now} = rt_i\)。
可持久化线段树
一般也是更改节点可持久化,能用标记永久化就尽量用标记永久化,pushdown 在一些情况下复杂度是对的也可以做。
可持久化权值线段树也利用可减性做题。
这个没有固定代码,就是修改的时候新增结点就好了。
可持久化平衡树
当分裂与合并的时候,复制一份,然后维护历史版本。
可持久化一般是 FHQ,但是这个东西的复杂度似乎不太对,但是很难卡,但是我知道可持久化 FHQ 的区间复制复杂度肯定是错的。
详细讲区间复制:把历史版本与当前版本合并就行。
来点合并分裂:
void split(int node, int val, int &r1, int &r2){
  if(!node){
    r1 = 0;
    r2 = 0;
    return ;
  }
  if(tree[node].val <= val){
    r1 = newnode();
    tree[r1] = tree[node];
    split(tree[r1].rs, val, tree[r1].rs, r2);
    update(r1);
  }
  else{
  	r2 = newnode();
  	tree[r2] = tree[node];
    split(tree[r2].ls, val, r1, tree[r2].ls);
    update(r2);
  }
}
int merge(int x, int y){
  if(!x || !y){
    return x + y;
  }
  if(tree[x].key > tree[y].key){
  	int node = newnode();
  	tree[node] = tree[x];
    tree[node].rs = merge(tree[node].rs, y);
    update(node);
    return node;
  }
  else{
  	int node = newnode();
  	tree[node] = tree[y];
    tree[node].ls = merge(x, tree[node].ls);
    update(node);
    return node;
  }
}
线段树 & 平衡树
线段树
主要是讲一些基本模型
考虑到线段树的基本结构,我们需要开 \(4n\) 个结点。
还有就是势能线段树,反正分析势能乱搞就对了(比如区间取 \(\min / \max\))
然后以下是基本总结:
线段树(Segment Tree)是一种基于树结构的数据结构,主要用于处理区间查询和区间更新操作。其基本原理如下:
数据结构:
- 线段树通常是一棵平衡二叉树。
- 每个节点代表数组中一段连续的区间,叶子节点对应数组中的单个元素。
构建过程:
- 线段树从根节点开始,递归地将数组划分为更小的子区间,直到每个叶子节点表示数组中的一个单独元素。
- 每个非叶子节点存储其子节点的合并结果(如区间和、最大值等)。
功能:
- 区间查询:线段树能快速求解任意区间的查询问题,如区间和、最小值、最大值等。
- 区间更新:支持快速更新数组中的元素,并且能够保持树结构的正确性。
操作复杂度:
- 构建:线段树的构建时间复杂度为 \(O(n)\),\(n\) 是数组长度。
- 查询:查询操作的时间复杂度为 \(O(\log n)\)。
- 更新:更新操作的时间复杂度为 \(O(\log n)\)。
应用场景:
- 适用于需要频繁进行区间查询和更新的场合,如动态统计区间内的最值、求和等问题。
- 在解决动态数组查询问题时,特别是针对静态区间的查询和更新操作时表现优异。
- 总结来说,线段树通过将数组元素组织成树结构,利用树的特性实现高效的区间查询和更新,是解决区间统计问题的一种经典数据结构。
平衡树
一般都是写 FHQ Treap。
如果是 Splay 就是单旋双旋旋到顶,可以动态适配复杂度(如 \(O(\log n)\) 的启发式合并),还有就是可以实现 LCT。
如果是替罪羊树就是重构,这一类平衡树在乱搞方面有极大优势(如 KDT 重构),但是基本不太好写,复杂度也有问题,但是好想。
然后附上分裂合并代码:
void split ( int node, int &r1, int &r2, int k ) {
    if ( !node ) {
        r1 = r2 = 0;
        return ;
    }
    if ( tree[node].val <= k ) { // 如果已经小于,整个左子树也是满足的,分裂右子树
        r1 = node;
        split ( tree[node].rs, tree[node].rs, r2, k );
    }
    else { //不满足,右子树肯定满足 > k,合并到 r2,分裂左子树
        r2 = node;
        split ( tree[node].ls, r1, tree[node].ls, k );
    }
    pushup ( node );
}
int merge ( int x, int y ) { // 需保证 x 子树值比 y 子数值小
    if ( !x || !y ) {
        return x + y;
    }
    if ( tree[x].key < tree[y].key ) { // 随机合并,期望 log
        tree[x].rs = merge ( tree[x].rs, y );
        pushup ( x );
        return x;
    }
    else {
        tree[y].ls = merge ( x, tree[y].ls );
        pushup ( y );
        return y;
    }
}
题目
P3261 [JLOI2015] 城池攻占
直接每个点维护左偏树,然后暴力往上合并即可,由于一个元素最多删除一次,所以复杂度有保证。
P1552 [APIO2012] 派遣
考虑到从下往上合并。
考虑一个人如果已经不能被选了,那么越往上走就越不能选,所以一个元素只会被丢一次,复杂度是对的。
P4755 Beautiful Pair
考虑笛卡尔树启发式合并,小的区间暴力,大的区间直接加入树状数组查询即可,注意要好好分析启发式合并的复杂度,否则会挂。
P3899 [湖南集训] 更为厉害
典题。
直接线段树合并统计贡献即可(发现深度是一段区间)。
P4592 [TJOI2018] 异或
对链和子树分别做可持久化 0/1 Trie 上的二分,然后就没了。
P3586 [POI2015] LOG
直接上平衡树统计一下贡献,判断一下贪心选择能否成功。
P4514 上帝造题的七分钟
考虑到空间限制不允许我们使用二维线段树,所以我们可以二维树状数组维护差分数组的四个项,然后二位前缀和一下。
数论
exgcd
通常来说,我们用辗转相除法求 \(\gcd (a,b)\)。
根据裴属定理,\(ax + by = c\) 有整数解当且仅当 \(\gcd ( a, b ) | c\),所以问题就被简化成了 \(ax + by = \gcd (i, j)\)。
然后我们考虑在辗转相除的过程中求,具体来说:
- 辗转相除已经解出 \(bx_1 + (a \bmod b)y_1 = \gcd ( a, b )\)
- 我们要求 \(ax_2 + b_x2 = \gcd ( i, j )\) 的解。
- 将上面化简得:\(bx_1 + (a - b\lfloor \frac{a}{b} \rfloor)x_2\)。
- 化简:\(ax_2 + b(x_2 - b \lfloor \frac{a}{b} \rfloor)\)。
- 然后继续递推就好了
Lucas 定理
就是拆成 \(p\) 进制下每一位的组合数相乘。
具体来说:
通常运用在模数较小,然后求逆元会挂得情况下(如果你看不懂,你得逆元得重新学)。
CRT
没用,不讲。
exCRT
就我们设两个东西公共解为 \(a\),列出二元一次方程,联立,然后不断重复解方程的过程就好了。
乘法逆元
若 \(ax \equiv 1 ( \mod p)\),则 \(a\) 与 \(x\) 在模 \(p\) 意义下互为乘法逆元。
使用场景:
- 若出现 \((\frac{a}{b} \bmod p)\),不能等价于 \(\frac{a \bmod p}{b \bmod p}\),此时可以用 \(a\) 乘以 \(b\) 的逆元 \(inv_b\),\(a \times inv_b \bmod p\)。
求解逆元:
- 求单个整数的逆元:
- 
- 扩展欧几里得算法:
 
\(ax \equiv 1 ( \mod p) \Rightarrow ax + py = 1\)
- 由此可知,乘法逆元不一定存在,因为方程并不一定有解。
- 乘法逆元若存在,那么有无数个,但在模 \(p\) 意义下只有一个(最小解)。
- 
- 有限情况下使用费马小定理:
 
费马小定理:若 \(p \in \mathbb{P}\),且 \(\gcd (a, p) = 1\),那么 \(a^{p - 1} \equiv 1 ( \mod p )\)
- \(a^{p - 1} \equiv 1 ( \mod p ) \Rightarrow a^{p - 2} \cdot a \equiv 1 ( \mod p )\),所以 \(a^{p - 2}\) 为 \(a\) 在模 \(p\) 意义下的逆元。
- 求 \(1 \sim n\) 的所有整数的逆元(前提是存在):
\(ax \equiv 1 ( \mod p )\),设 \(p = a \cdot q + r\),其中 \(q = \lfloor \frac{p}{a} \rfloor, r = p \bmod a\),则 \(p = a \cdot q + r \equiv 0 ( \mod p)\),\(a \equiv -r \cdot inv_q ( \mod p ), inv_a = -q \cdot inv_{p \bmod a}\),那么 \(inv_a = \frac{-q}{a} \cdot inv_{p \bmod a}\)
- 注意前提要存在。
- 线性求逆元:
- 先预处理阶乘,然后处理出 \(n!\) 的逆元,每次都乘上 \(i\) 就可以线性求解 \(1 \sim n\) 的阶乘的逆元了,这个用的一般比较多,但是大部分时候用 \(\log\) 就够了。
数论 \(2\)
基本数论
数论函数:定义域为正整数的函数。
欧拉函数:定义 \(\varphi(n) = \sum\limits_{i = 1}^n [\gcd(i, n) = 1]\)。
特殊点:
- \(\varphi(1) = 1\)。
特殊性质:
- 如果 \(p \in \mathbb{P}\),\(\varphi(p^n) = p^{n - 1}(p - 1)\),证明:
与 \(p\) 不互质的 \(x\) 一共有 \(p^{n - 1}\) 个(含有 \(p\)),所以 \(\varphi (p) = p^n - p^{n - 1}\),证毕。
- 若 \(a | x\),则 \(\varphi(ax) = ax\),证明:
分成 \(1 \sim x, 2x \sim 3x ...\) 若干个区间,我们设 \(1 \sim x\) 里有 \(p_1, p_2 ...\) 与 \(ax\) 互质,那么 \(p_1 + x, p_2 + x ...\) 仍然与 \(ax\) 互质,且只存在这些在 \(x \sim 2x\) 之间的数与 \(ax\) 互质,因为若有一个另外的数 \(p_3\),那么 \(p_3 - x\) 理应与 \(ax\) 也互质,推翻假设,证毕。
- 若 \(\gcd ( a, b ) = 1\),\(\varphi(ab) = \varphi (a) \varphi (b)\),证明:
欧拉函数是积性函数。
欧拉函数的求法:
- 用定义法求单个 \(x\) 的欧拉函数 \(\varphi( x )\),公式如下:
\(\varphi(x) = x \cdot \frac{p_1 - 1}{p_1} \cdot \frac{p_2 - 1}{p_2} \cdot ...\)。
- 同线性筛求 \(1 \sim n\) 以内所有数值的欧拉函数,具体操作:
void init () {
	phi[1] = 1;
	for ( int i = 2; i <= 1000000; i ++ ) {
		if ( !vis[i] ) {
			prime[++ cnt] = i;
			phi[i] = i - 1;
		}
		for ( int j = 1; j <= cnt && i * prime[j] <= 1000000; j ++ ) {
			vis[i * prime[j]] = true;
			if ( !( i % prime[j] ) ) {
				phi[i * prime[j]] = phi[i] * prime[j];
				break;
			}
			phi[i * prime[j]] = phi[i] * ( prime[j] - 1 );
		}
	}
}
莫比乌斯反演
就数论函数典型套路,然后讲莫比乌斯反演。
就是 \(\sum_{d | n }\mu(d) = [n = 1]\)。
然后记住一些数论函数间的乘积关系,不过大部分没用。
然后一些典型推柿子:
- 转换变量,枚举最大公约数。
- \(d(ij) = \sum_{x | i} \sum_{y | j} [\gcd(x, y) = 1]\)
- 然后进行区域划分,分块套分块。
- 整理式子,改变顺序。
- 充分理解。
没有什么别的,就是推式子。
然后放一些经典例题(其实杜教筛学没学都一样,反正不会考)。
题目
整除分块
记录一下:\(r = n / ( n / l ), l = r + 1\)。
主要就是利用除法的连续性。
P3327 [SDOI2015]约数个数和
又是一道清新的莫比乌斯反演(真是令人作呕)。
Tm 的想了 \(5\) 分钟没有想到怎么推柿子结果是 \(d\) 的性质没了解到。这道题会 \(d\) 的性质就秒秒钟切。
\(d\) 的性质:
有了这个性质就秒秒钟切,先令 \(n \le m\):
根据直觉可得:
很显然 \([p|x][p|y] = 0\) 是无用的:
然后发现 \(\left\lfloor \frac{n}{p i} \right\rfloor = \left\lfloor \frac{\frac{n}{p}}{i} \right\rfloor\),此时你可以预处理出里面的数,外面和里面可以用整除分块搞。
P4213 【模板】杜教筛(Sum)
包含着很多悲伤的故事(指卡 long long)。
考虑对于任意积性函数 \(f(x)\) 来说,如果你要求 \(S(n) = \sum\limits_{i = 1}^n f(i)\),你该怎么办。
正常做法是 \(O(n)\),毒瘤们非常不爽,于是有了杜教筛。
首先我们需要知道狄利克雷卷积是什么:
我们定义 \(f, g\) 为两个数论函数,则 \(h = f * g\),有如下等式:
其实这个东西可以化成卷积的形式:
只不过上面那种形式比较通用。
考虑杜教筛的原理,先构造 \(h, g\),使得 \(h = f * g\),则:
然后:
然后我们发现 \(S\left(\left\lfloor\frac{n}{d}\right\rfloor\right)\) 是可以整除分块算的,我们只需快速算出 \(\sum_{i = 1}^n h(i)\) 即可,然后我们递归求解即可。
其复杂度为 \(O(n^{\frac{2}{3}})\),其实没啥用。
P5221 Product
感觉很难欸。
首先根据我们的瞪眼法不难发现 \(104857601\) 是一个质数,这在后面非常有用。
然后你发现 \(\text{lcm}\) 这东西,我熟啊,直接推柿子:
先看分子:
解释一下这里为什么,你考虑当 \(i\) 不变的情况下,$j = 1, 2, ..., n $ 的情况就 OK 了。
然后再看分母(公式警告):
这里很有道理吧!
然后我们只看指数:
我们发现,对于每个 \(i\) 来说,有贡献的只有与他互质的数,这种计数方法在 GCD SUM 这道题里有所体现:
那么我们便要求:
然后你提交一发,发现 long long 会炸!所以我们需要用到欧拉定理:
当 \(\gcd (a, k) = 1\) 时,有 \(a^b = a^{b \bmod \varphi(k)}\),此时我们的模数时质数,所以给指数模一下 \(104857601 - 1\) 就行了。
然后就是疯狂注意细节。
概率论
基本定义
我们定义 \(P(A)\) 为事件 \(A\) 发生的概率,\(P(\overline{A})\) 为事件 \(A\) 不发生的概率。
然后是一些定义:
- \(\Omega\):定义为样本空间,为随机试验所有结果构成的集合。
- 若 \(A\) 的所有结果在 \(\Omega\) 中有出现,则称 \(A\) 事件发生。
- 概率 \(P(A)\) 为 \([0, 1]\) 的数,满足 \(P(\Omega) = 1\) 且 \(P(A) = \sum_{x \in A}P({x})\)。
- 定义 \(\overline{A} = \Omega - A\),\(P(A) + P(\overline{A}) = 1\)。
- \(P(A \cup B) = P(A) + P(B) - P(A \cap B)\)。
- 若 \(A, B\) 为独立事件,\(P(A \cap B) = P(A) \cdot P(B)\)。
- 若 \(A, B\) 为互斥事件,\(P(A \cap B) = 0\) 且 \(P(A \cup B) = P(A) + P(B)\)。
- 条件概率,\(P(A|B)\) 定义为 \(B\) 发生的情况下,\(A\) 发生的概率,\(P(A \cap B) = P(B) \cdot P(A|B) = P(A) \cdot P(B|A)\)。
然后是期望的一些定义:
- 定义变量 \(X\) 在随机试验中可能取值为 \(x_1, x_2, ... x_n\),那么定义 \(X\) 的期望 \(E(X) = \sum_{i = 1}^{n} x_i \cdot P(X = x_i)\)。
全概率公式
定义 \(\Omega\) 的一个划分 \(B_i (i = 1, 2, 3 ...)\), \(\forall i \ne j, B_i \cap B_j \ne \emptyset\) 且 \(\cup_{i} B_i = \Omega\),则有:
贝叶斯公式
但是貌似在信息中冒得用?
整数期望公式
相当于把贡献摊到后缀和上。
期望的线性性
定义两个变量 \(X, Y\),那么满足在同一随机试验中:
全期望公式
还是取划分 \(B_i\),则有:
然后就是一些题了。
贪心
主要是一些经典套路,主要是讲题。
P1181 数列分段 Section I
首先这是一个经典的序列贪心问题。
- 
贪心结论:从开头开始,能选的尽量选,不能选的单开一段,这样是最优的。 
- 
证明:考虑如果不尽量选,把 \(a_i\) 划分到下一段里,那么很明显下一段的结束位置会前移,肯定对划分段数来说是不优的,所以我们就要尽量选。 
所以有的时候盲目猜最优结论大概率是对的。
P1223 排队接水
一个经典的匹配问题。
- 
贪心结论:从小到大排序,依次加入。 
- 
证明:考虑是要排序完后前缀和的和最小,肯定是小的排在前面造成更多的贡献,大的在后面造成更少的贡献更好。 
P1190 [NOIP2010 普及组] 接水问题
- 
贪心结论:从小到大排序,哪个水龙头没人就往哪去。 
- 
证明:考虑到从小到大一定是优的,不等待也一定是优的,所以和一起也一定是优的。 
P1094 [NOIP2007 普及组] 纪念品分组
- 
贪心结论:一个大的配一个小的,如果配不了就只选一个大的。 
- 
证明:大的是很难处理的,考虑至多两件,肯定是一个大的能配上一个其他的最优,考虑配最小的一定是涵盖了所有情况的,如果配小的都配不了,那就都配不了了。 
P1080 [NOIP2012 提高组] 国王游戏
- 
贪心结论:按照 \(a_i \times b_i\) 从小到大排序即是最优结果。 
- 
证明:经典套路,邻项交换,把式子写出来,再提出 \(\max\) 化简一下就好了。 
P4377 [USACO18OPEN] Talent Show G
考虑 \(0/1\) 分数规划,化成整式后然后二分答案一下里面套个背包就好了。
P6187 [NOI Online #1 提高组] 最小环
考虑到一个置换环的长度为 \(\gcd(k, n)\),也就是说最多就 \(\sqrt n\) 个,所以我们考虑每个置换环咋做然后预处理这题就做完了。
考虑贪心的选择,最大的随便放,从大往小依次放,放到最大的旁边,这样就一定是大的乘大的,小的乘小的,肯定是最优的,对于每种长度的环预处理出来贡献就好了。
计算几何
学习了计算几何,内容有点多,下面讲的比较简略。
声明:如无特别声明,所有点 \(A\) 一律表示向量 \(\overrightarrow{OA}\)。
Part 1. 二维向量部分
- 
判断两向量是否共线:\(x_1y_2=x_2y_1\) 
- 
向量的点积:\(a\cdot b=x_1\times x_2+y_1\times y_2=|a||b|\cos<a,b>\) 
- 
正弦定理:\(\dfrac{a}{\sin A}=\dfrac{b}{\sin B}=\dfrac{c}{\sin C}=2R\)。 
- 
余弦定理:对于 \(\triangle ABC\),有 \(\begin{cases} a^2=b^2+c^2-bc\cos A \\b^2=a^2+c^2-ac\cos B \\c^2=a^2+b^2-ab\cos C \end{cases}\) 
- 
向量的叉乘:\(a\times b=x_1\times y_2-x_2\times y_1=|a||b|\sin<a,b>\) 
- 
向量的旋转:设向量 \(a=(x,y)\),倾角为 \(\theta\),长度 \(l=\sqrt{x^2+y^2}\),设其逆时针旋转 \(\alpha\) 度,则原向量的 \(x=l\cos\theta,y=l\sin\theta\),旋转后变为 \(x'=l\cos(\theta+\alpha),y'=l\sin(\theta+\alpha)\)。 
- 
存储直线:通过直线上一个点以及方向向量进行存储,记为 \((x,\overrightarrow{u})\)。 
Part 2. 判断问题
- 折线段拐向判断(顺时针或逆时针)
判断 \((p_1-p_0)\times (p_2-p_1)\) 的正负性。
- 判断点 \(Q\) 是否在线段 \(P_1P_2\) 上
先判断 \(P_1,P_2,Q\) 是否三点共线(变成两个向量叉乘判断),然后判断 \(Q\) 是否在以 \(P_1P_2\) 为对角线构成的矩形中即可。
- 判断两线段 \(P_1P_2,Q_1Q_2\) 是否相交
分两步走:
- 快速排斥实验
即判断两线段所夹的矩形(即以线段为对角线构成的矩形)是否相交,若不相交显然两线段也不相交。
- 跨立实验
即判断一条线段的两个端点是否分布在另一条线段的两侧,可以用向量叉乘来判断,即判断 \((P_1-Q_1)\times(Q_2-Q_1)\ *\ (Q_2-Q_1)\times(P_2-Q_1)\ge 0\)。
- 判断点,线段,折线,多边形是否在矩形中
判点很简单,只需要对长和宽分别限制即可。由于矩形具有凸性,所以其他的图形只要对每一个点判一下即可。
- 判断圆是否在矩形中
判断圆心是否在矩形中,再判断圆心到矩形的最短距离是否大于等于半径。
- 判断一点是否在任意多边形内部
有两种算法:
光投射算法就是以该点为端点引出一条射线,判断其与多边形边的相交次数是否为奇数。具体实现可以把射线看做一条长线段(外面的端点可以随机取),然后对每条边做跨立实验即可。
但是如果交点是端点的话会出问题,所以我们规定若相交的端点是其所处边上纵坐标更大的点则计入贡献,这样则我们不考虑水平边端点的贡献。
如果点就在多边形某个点或边上,则直接判断成立。
回转数算法就是让该点与多边形每个点连边,计算相邻两边的夹角和(注意这里夹角有向),若和为 \(0\) 则在其外部,否则在内部。但是这种做法精度误差会比较大。
- 判断线段是否在多边形内
先判断端点,再判断边两两之间是否相交,如果交点是线段端点或者某一条边的端点则需要特殊考虑,把所有端点按照横纵坐标排序,然后判断相邻两点的中点是否也在多边形内即可。
这样的最坏时间复杂度是 \(O(n^2)\) 的,因为最坏取 \(O(n)\) 级别个点,然后求点在多边形内是 \(O(n)\) 的。目前并没有想到什么好方法进行优化。
- 判断折线,矩形,多边形是否在多边形内
拆成一些边进行判断即可。
- 判断圆是否在多边形内
判断圆心是否在多边形内,以及判断圆心离多边形每条边的距离最小值是否大于等于半径。
- 判断点是否在圆内
判断点到圆心距离是否不超过半径
- 判断线段,折线,矩形,多边形是否在圆内
考虑圆是凸性的,判断每个端点即可。
- 判断圆是否在圆内
判断 \(\operatorname{dis}(P_1,P_2)\le r_1-r_2\) 即可。
Part 3. 计算问题
- 计算点与直线之间的距离
\(\dfrac{(P_2-P_1)\times (P-P_1)}{|P_2-P_1|}\)
- 计算点与线段之间的距离
若线段与点构成的三角形为钝角三角形(即向量 \(P_2-P_1\) 与 \(P-P_1\) 的夹角大于 \(90°\) 或向量 \(P_1-P_2\) 与 \(P-P_2\) 的夹角大于 \(90°\)),则答案为点到两个端点距离的最小值,否则化为点与直线的距离。
- 计算点到折线,矩形,多边形的距离
对每条边做一次点与线段的距离即可。
- 计算点到圆的距离与交点坐标
若该点为圆心则距离为半径,有无数个交点。
否则距离为 \(|r-|\overrightarrow{OP}||\),交点为 \(\overrightarrow{OP}\) 乘以 \(\dfrac{r}{\overrightarrow{OP}}\)。
- 计算两条共线线段的交点
设较长线段为 \(l_1\),较短线段为 \(l_2\)。若 \(l_1\) 不包含 \(l_2\) 则无交点,否则若 \(l_1\) 包含 \(l_2\) 的一个端点且该端点也是 \(l_1\) 的端点则其为交点,否则有无数个交点。
- 计算两条线段(直线)的交点
首先判断是否相交,若不相交则没有交点,若共线则按共线的方法去判。
否则交点 \(O=x_1+u_1\dfrac{u_2\times(x_2-x_1)}{u_2\times u_1}\)。
- 求线段(直线)与折线,矩形,多边形的交点
对每一条边求交点即可。
- 求线段(直线)与圆的交点
首先判断是否相离,即判断圆心到线段(直线)的距离是否大于半径,若大于则相离。
对于直线而言,使圆心朝直线作垂线,求出垂足坐标,然后用勾股定理求出偏移量,在直线上移动相应距离即可。对于线段求出后还要判断求出来的点是否在线段上。
- 求圆与圆的交点
首先判断是否外离或者内含,这两种情况都是没有交点。
然后判断是否相切,如果相切则算出连接两圆心的方向向量与偏移距离,然后移动即可。
否则相交,此时可以求出圆心距,然后我们知道以圆心距和两个圆心到其中一个交点的距离(就是两个半径),可以用余弦定理求出两圆心连线与一圆心与交点连线的角度,然后分别向两个方向旋转后对应移动即可。
- 求圆与圆的公切线
分讨一下,最基础的重合与内含不再赘述,如果内切则有一条外公切线,如果相交则有两条外公切线,如果外切则有两条外公切线和一条内公切线,如果外离则各有两条外公切线和内公切线。
内切好办,两圆心相连后移动大圆半径并作垂线即可(好像变成公切点了)
然后自己画个图就能得到其他情况下内公切线的两圆心连线与一个圆心到切点的连线的夹角是 \(\arg \cos(\frac{(r_1+r_2)}{d})\),而外公切线则是 \(\arg \cos(\frac{(r_1-r_2)}{d})\),注意这里默认 \(r_1>r_2\),\(d\) 是圆心距。
- 求多边形面积
取一个在多边形内的点(可以是端点),然后三角剖分求解即可。
- 三角形重心公式:\(x=\dfrac{x_A+x_B+x_C}{3},y=\dfrac{y_A+y_B+y_C}{3}\)。
一道好题:P3738
这题由于直线与 \(y\) 轴平行所以可以简化判断直线与线段相交的过程,交点也更加好求。
考虑枚举每条边求与直线的交点,若交点不在端点则可以直接记录,否则还需要判断该点是否有效。判断点是否有效只需要判断连接该点的两条边是否在直线两侧即可。
然后就是一些细节问题。
Part 4. Pick 定理
对于一个顶点在格点上的多边形,有 \(S=a+\dfrac{b}{2}-1\),其中 \(a\) 为多边形内部的点,\(b\) 为多边形边上的点。
它主要用于求格点数而不是求面积,虽然还是不常见。
Part 5. 距离
- 
欧几里得距离:即直线距离,表示为 \(|AB|=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\)。 
- 
曼哈顿距离:即在格点上行走的距离,表示为 \(|AB|=|x_1-x_2|+|y_1-y_2|\)。 
- 
切比雪夫距离:即在格点上向两个方向行走的最大值,表示为 \(|AB|=\max\{|x_1-x_2|,|y_1-y_2|\}\)。 
然后自己画图就能得到 \((x,y)\to(x+y,x-y)\Leftrightarrow曼哈顿距离\to 切比雪夫距离\),\((x,y)\to(\dfrac{x+y}{2},\dfrac{x-y}{2})\Leftrightarrow切比雪夫距离\to 曼哈顿距离\)。
Part 6. 凸包
凸包就是包含给定点集且面积最小的凸多边形,而凸壳就是凸包上斜率不变的一部分,显然凸包由上下凸壳组成。
- Andrew 算法求凸包
Andrew 算法分别求两个凸壳,简单来说就是开栈记录,每压入一个点就弹出栈顶直到斜率不变,顺序扫下凸壳,倒序扫上凸壳,复杂度 \(O(n)\),但是你还要按坐标排序,所以带 \(\log\)。
- 动态增点求凸包
开两颗平衡树或 set 维护上下凸壳,内部元素按坐标排序,然后分别对前驱后继的不合法点进行删除,不合法判定为左右两边的点能够覆盖这个点。
- 动态增删点求凸包
可以用 cdq 分治做,强制在线可做不好做。
Part 7. 半平面交
- 
半平面,是一条直线的某一边的所有点所构成的点集。 
- 
半平面交就是求若干条直线的半平面交集,显然半平面交也是一个点集。 
- 
最常见的做法是求半平面交面积,方法如下: 
- 
把所有边按逆时针顺序标向,这样每条边都可以看做一个向量,然后我们默认是求所有直线的左半平面交(右半平面交则按顺时针标向)。 
- 
把这些向量按极角排序,若极角相同,靠左的排在前面。 
- 
去重,极角相同的向量保留左侧的。 
- 
分别尝试将每个向量压入双端队列,如果队尾的两个向量的交点在该向量右侧则弹出队尾,队首同理,然后将该向量压入队尾。 
- 
最后看队首队尾是否合法,弹出不合法的队首和队尾。 
求面积的话对向量两两求交点,然后用向量叉积求解即可。
- 
多边形的核,即多边形中的一个点集,满足点集中的每一个点与多边形边上每一个点连成的线段均在多边形内,可以用半平面交求解。 
- 
求多边形内接圆最大面积:二分一个半径 \(r\),将每条边向内移动 \(r\) 个单位长度,然后求多边形的核是否还存在即可。 
Part 8. 旋转卡壳
旋转卡壳可以用来解决以下问题:
- 求凸多边形直径
仍然可以人类智慧:直接考虑按照下面平面最近点对的方法排序,我们取前 \(5\) 个点和后 \(5\) 个点进行计算,就 OK。
考虑枚举每一条边,对每条边找到离他最远的点,然后尝试去更新答案。这样时间复杂度显然是 \(O(n)\)。
也可以用来求平面最远点对。宽度改为求点到边的最短距离。
- 求凸多边形间最大距离
其实就是两根平行线分别放在两个凸多边形上,然后一个顺时针转一个逆时针转。注意最开始的时候一个放在 \(x\) 坐标最小的点上,一个放在 \(x\) 坐标最大的点上。
最小同理,但是还要考虑点边与边边的贡献。
- 求最小面积外接矩形
用两对平行线旋转,第一对转到与边重合后第二对再转,转到与第一对垂直后更新答案,然后继续旋转。最小周长同理。
Part 9. 随机增量法
就是构造一种做法使得时间复杂度为 \(T(n)=T(n-1)+\sum_{i=1}^nP(x=i)g(i)\),如果式子中的概率较小那么随机打乱数据后时间复杂度就是 \(O(n)\)。
- 最小圆覆盖问题
随机打乱后维护前 \(i-1\) 个点的最小圆覆盖,枚举每个点,若该点在最小圆覆盖以外则重新构造最小圆覆盖,具体是以该点为圆心枚举第二个不在该圆中的点,然后以这两个点作圆后再枚举第三个不在该圆中的点,以这三个点作三角形外接圆即可。
这样设枚举三个点的时间复杂度分别为 \(F(n),G(n),H(n)\),那么有
所以 \(T(n)=O(n)\)。
Part 10. 平面最近点对
来补充一个人类智慧:
- 
出题人可能会用一组 \(x\) 很接近,\(y\) 相差很大的数据来卡你,于是你随机旋转一个角度,然后再按照 \(x \times x + y \times y\) 排序,然后根据数学直觉离得很近的两个点肯定在数组中不会相差太远,于是我们取每个点的前 30 个数进行计算。 
- 
或者我们还可以按照 \(x\) 排序,如果 \(x\) 之间的差要大于当前答案就可以直接跳了,不难发现随机旋转一个角度后这个东西仍然是对的。 
采用分治法:
- 
将每个点按照 \(x\) 坐标为第一关键字,\(y\) 坐标为第二关键字进行排序。 
- 
在区间 \([l,r]\) 中选取 \(mid=\frac{l+r}{2}\),其中 \([l,r]\) 表示下标区间,递归求解 \([l,mid]\) 和 \([mid+1,r]\) 的答案,更新最小答案,记为 \(h\)。 
- 
考虑跨越两部分的点对贡献,从点 \(p_mid\) 向左右各延伸 \(h\) 个单位,显然只有这一部分的点对才有可能造成贡献。 
- 
把这一部分的点按 \(y\) 坐标排序(可使用归并排序)。 
- 
依次考虑这些点,对每个点向下枚举满足两个点在 \(y\) 坐标上的距离不超过 \(h\)。 
- 
更新答案。 
这样的时间复杂度是 \(O(n\log n)\),证明略。
入门题,枚举每条边,若交点不在端点上则使用相似三角形法求出交点(本题精度误差不大),在端点上则还要判断连接该端点的两条边是否在直线两侧。最后分入点出点按 \(y\) 坐标排序求解即可。
不考查如何建凸包,而是考察对凸包的理解。
考虑对点两两连边,然后按极角排序,那么此时我们枚举起始边顺序 DP 即可。时间复杂度 \(O(n^3)\)。
博弈论
Part 1. Nim 游戏
有 \(n\) 堆石子,第 \(i\) 堆有 \(a_i\) 个石子,每次可以选择一堆石子并取走该堆的若干个石子,最先不能取石子的人输,求是否有先手必胜策略。
定义必胜状态为先手必胜的状态,必败状态为先手必败的状态。
显然有以下三条定理:
- 
没有后继状态的状态是必败状态 
- 
必胜状态一定有至少一个后继状态为必败状态 
- 
必败状态任何一个后继状态为必胜状态 
那么如果我们绘制出博弈图,时间复杂度就是点数加边数级别的,在该游戏下为 \(O(\prod a_i)\)。
显然这样的时间复杂度太高,因此需要优化。
考虑构造一个数列使得它随状态的转移而转移,且满足上面三条定理。(这是最难的地方)
经过构造,我们发现 \(\oplus a_i\) 满足以上三条定理。令 \(\oplus a_i=0\) 时为必败状态,否则为必胜状态。
- 
显然满足定理 \(1\),因为全 \(0\) 时异或和也为 \(0\)。 
- 
若 \(\oplus a_i=k\),则下次操作后需要把某个 \(a_i\) 异或 \(k\)。因为一定有一个 \(a_i\) 在 \(k\) 的最高位为 \(1\),所以总能找到一堆石子使得取走若干石子后异或和为 \(0\)。 
- 
若 \(\oplus a_i=0\),则下次操作后想要让 \(\oplus a_i\) 仍然等于 \(0\),显然只能对一堆石子异或 \(0\),而这是不合法的。 
综上,\(\oplus a_i=0\) 满足以上三条定理,所以可以用它来判断是否有先手必胜策略。
Part 2. SG 函数与 SG 定理
考虑将 Nim 游戏转换成有向图游戏,即给定一张有向图,只有一个没有入度的点,有一枚棋子在这个点,A 和 B 同时移动棋子,最先不能移动的人输,求是否有先手必胜策略。
我们设 \(sg(i)\in\{0,1\}\) 表示该点的状态,\(sg(i)=0\) 表示先手必败状态,显然有 \(sg(x)=\operatorname{not}(\operatorname{and}_{(x,v)\in E}sg(v))\)。
那么如果有多个 DAG 呢?此时显然不能再像上面一样定义 \(sg\) 了,因为对于每个 DAG 先后手顺序可能改变。
定义运算 \(\operatorname{mex}(S)\) 表示 \(S\) 中最小未出现的自然数,重新定义 \(sg(x)=\operatorname{mex}_{(x,v)\in E}(sg(v))\),没有出度的点 \(sg(i)=0\)。
- SG 定理:若对于每一个 DAG 的起点 \(s_i\),有 \(\oplus sg(s_i)=0\),则先手必败,否则先手必胜。
Part 3. 泛化 Nim 和
如果 Nim 游戏中每次取一堆改为每次取 \(k\) 堆,那么判断每一个二进制位中 \(1\) 的个数是否为 \(k+1\) 的倍数,若是则先手必败,否则先手必胜。
证明与普通 Nim 游戏基本相同。
不难发现,此题只需要判断 \(|n-m|\le 1\) 即可,以下是证明:
- 
若最终状态是 \((0,0),(1,0),(0,1)\),显然先手必败,满足 \(|n-m|\le 1\)。 
- 
若 \(|n-m|\le 1\),设 \(n>m\),则取 \(2x\) 个石子后差变为 \(|n-m-3x|\),显然这个值 \(>1\)。 
- 
若 \(|n-m|>1\),设 \(n>m\),则取 \(2x\) 个石子后差变为 \(|n-m-3x|\),显然一定有一个 \(x\) 使得这个值 \(\le 1\)。 
好题
我们在黑白棋一题中可以发现有的时候考察博弈论可能需要记住一些结论。
这道题是 K-Nim 的结论,然后我们将 \(1\) 的个数的值计入一个 DP 状态中进行 DP 即可。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号