add files
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,949 @@
|
||||
# 基础算法
|
||||
|
||||
- [基础算法](#基础算法)
|
||||
- [二分](#二分)
|
||||
- [二分的应用](#二分的应用)
|
||||
- [二分答案](#二分答案)
|
||||
- [实数二分](#实数二分)
|
||||
- [二分寻找函数零点](#二分寻找函数零点)
|
||||
- [二分0/1分数规划](#二分01分数规划)
|
||||
- [二分最小比值生成树](#二分最小比值生成树)
|
||||
- [二分延伸-实数三分](#二分延伸-实数三分)
|
||||
- [三分求连续函数的极值点](#三分求连续函数的极值点)
|
||||
- [三分优化](#三分优化)
|
||||
- [二分答案代码](#二分答案代码)
|
||||
- [实数二分代码](#实数二分代码)
|
||||
- [实数三分代码](#实数三分代码)
|
||||
- [二分三分例题](#二分三分例题)
|
||||
- [CF1355E Restorer Distance](#cf1355e-restorer-distance)
|
||||
- [CF1355E题解](#cf1355e题解)
|
||||
- [CF1355E参考代码](#cf1355e参考代码)
|
||||
- [分治](#分治)
|
||||
- [分治概括](#分治概括)
|
||||
- [分治应用](#分治应用)
|
||||
- [分治求逆序对](#分治求逆序对)
|
||||
- [分治例题](#分治例题)
|
||||
- [CF436D Pudding Monsters](#cf436d-pudding-monsters)
|
||||
- [CF436D题解](#cf436d题解)
|
||||
- [搜索进阶](#搜索进阶)
|
||||
- [搜索](#搜索)
|
||||
- [带剪枝的搜索](#带剪枝的搜索)
|
||||
- [启发式搜索](#启发式搜索)
|
||||
- [迭代加深搜索](#迭代加深搜索)
|
||||
- [倍增](#倍增)
|
||||
- [RMQ与ST表](#rmq与st表)
|
||||
- [RMQ与ST表应用](#rmq与st表应用)
|
||||
- [ST表求区间最大值](#st表求区间最大值)
|
||||
- [ST表的优劣](#st表的优劣)
|
||||
- [倍增例题](#倍增例题)
|
||||
- [P2048 NOI2010 超级钢琴](#p2048-noi2010-超级钢琴)
|
||||
- [P2048暴力代码](#p2048暴力代码)
|
||||
- [P2048题解](#p2048题解)
|
||||
- [例题2](#例题2)
|
||||
- [例题2题解](#例题2题解)
|
||||
- [P1084 疫情控制](#p1084-疫情控制)
|
||||
- [P1084题解](#p1084题解)
|
||||
- [P3295 萌萌哒](#p3295-萌萌哒)
|
||||
- [P3295朴素做法](#p3295朴素做法)
|
||||
- [P3295正解](#p3295正解)
|
||||
- [双指针](#双指针)
|
||||
- [双指针例题](#双指针例题)
|
||||
- [双指针例题1](#双指针例题1)
|
||||
- [双指针例题1题解](#双指针例题1题解)
|
||||
- [双指针例题1代码](#双指针例题1代码)
|
||||
- [双指针例题2](#双指针例题2)
|
||||
- [双指针例题2题解](#双指针例题2题解)
|
||||
- [双指针例题2代码](#双指针例题2代码)
|
||||
- [P1381 单词背诵](#p1381-单词背诵)
|
||||
- [P1381题解](#p1381题解)
|
||||
- [P1381代码](#p1381代码)
|
||||
- [折半搜索](#折半搜索)
|
||||
- [哈希,KMP](#哈希kmp)
|
||||
- [贪心](#贪心)
|
||||
- [构造转换模拟](#构造转换模拟)
|
||||
- [CF1916D Mathematical Problem](#cf1916d-mathematical-problem)
|
||||
- [CF1916D题解](#cf1916d题解)
|
||||
- [CF1916D参考代码](#cf1916d参考代码)
|
||||
- [CF1912E Evaluate It and Back Again](#cf1912e-evaluate-it-and-back-again)
|
||||
- [CF1912E题解](#cf1912e题解)
|
||||
- [CF1912E参考代码](#cf1912e参考代码)
|
||||
- [CF739A Alyona and mex](#cf739a-alyona-and-mex)
|
||||
- [CF739A题解](#cf739a题解)
|
||||
- [CF739A代码](#cf739a代码)
|
||||
- [CF1495C Garden of the Sun](#cf1495c-garden-of-the-sun)
|
||||
- [CF1495C题解](#cf1495c题解)
|
||||
- [CF1495C代码](#cf1495c代码)
|
||||
|
||||
## 二分
|
||||
|
||||
### 二分的应用
|
||||
|
||||
#### 二分答案
|
||||
|
||||
[代码](#二分答案代码)
|
||||
|
||||
#### 实数二分
|
||||
|
||||
[代码](#实数二分代码)
|
||||
|
||||
##### 二分寻找函数零点
|
||||
|
||||

|
||||
|
||||
单个:一次实数二分
|
||||
|
||||

|
||||
|
||||
多个:分割成段,多次二分
|
||||
|
||||
##### 二分0/1分数规划
|
||||
|
||||
给定等长数列 $a_i$, $b_i$, 求同样等长的数列 $w_i (w_i \in{0,1})$ , 最大化(或最小化) $\frac{\sum_i w_ia_i}{\sum_i w_ib_i}$。
|
||||
|
||||
考虑二分 $\frac{\sum_i w_ia_i}{\sum_i w_ib_i}$ 的最大值,在每一次二分中,相当于是验证 $\max{\frac{\sum_i w_ia_i}{\sum_i w_ib_i} \ge mid}$ 是否成立。
|
||||
|
||||
经过化简,每次二分的 $check$ 则变为了判断是否存在一组 $w_i$ 使 $\sum_iw_i(a_i-mid\times{b_i})\ge0$
|
||||
|
||||
##### 二分最小比值生成树
|
||||
|
||||
在一张无向连通图中,边具有 $a_i,b_i$ 两个权值,最小比值生成树是使 $\frac{\sum{a[i]}} {\sum{b[i]}}$ 最小的生成树
|
||||
|
||||
考虑用 $w_i (w_i \in{0, 1})$ 表示每条边是否选入树中,则该问题可转换为[01 分数规划问题](#二分01分数规划)
|
||||
|
||||
所以,我们令树上每一条边的边权为 $c_i(c_i = a_i - mid \times{b_i})$,然后便可转换为朴素最小生成树问题
|
||||
|
||||
#### 二分延伸-实数三分
|
||||
|
||||
##### 三分求连续函数的极值点
|
||||
|
||||
要求:单峰或单谷函数,峰的两侧必须是单调增或单调减
|
||||
|
||||
若不满足:切片后多次三分
|
||||
|
||||
过程:以单峰函数为例,设当前范围为 $[l, r]$,则取其三等分点 $lmid, rmid$,判断 $f(lmid)$ 与 $f(rmid)$ 的大小关系。
|
||||
|
||||
若 $f(rmid) > f(lmid)$ 则可以排除 $[l, lmid]$ 部分。
|
||||
|
||||
特别地,若 $f(rmid) = f(lmid)$,缩小范围可以直接缩减到 $[lmid, rmid]$ 范围(但是为了代码简洁,一般直接分到大于或小于部分,反正对时间复杂度影响不大:D)。
|
||||
|
||||

|
||||
|
||||
[代码](#实数三分代码)
|
||||
|
||||
### 三分优化
|
||||
|
||||
在二分与三分中,每次均取的是等分点,而实际上,不取等分点也可以进行二分或三分。
|
||||
|
||||
在二分中,假设每次取偏左的点,则如果搜索值在左边,就可大大缩减搜索量。但是如果搜索值在右边,则每次排除的区间就大大减少了。所以在二分中,一般取最中间的点。
|
||||
|
||||
而在三分中,如果让 $lmid$ 与 $rmid$ 尽量靠近,比如取 $lmid = \frac{50l + 49r}{99}, rmid = \frac{49l + 50r}{99}$,就可以将时间复杂度从 $O(log_{\frac{3}{2}})$ 优化到 $O(log_{\frac{99}{49}})$。
|
||||
|
||||
理论上,在[实数三分](#二分延伸-实数三分)中, $lmid, rmid$ 可以无限接近,但是要注意精度。
|
||||
|
||||
### 二分答案代码
|
||||
|
||||
```cpp
|
||||
// 二分答案
|
||||
int l = 0, r = n;
|
||||
while (l <= r)
|
||||
{
|
||||
int mid = l + r >> 1;
|
||||
if (check(mid))
|
||||
l = mid + 1;
|
||||
else
|
||||
r = mid - 1;
|
||||
}
|
||||
return l;
|
||||
```
|
||||
|
||||
### 实数二分代码
|
||||
|
||||
```cpp
|
||||
// 实数二分
|
||||
double l = 0, r = n;
|
||||
while (r - l > eps)
|
||||
{
|
||||
double mid = (l + r) / 2;
|
||||
if (check(mid))
|
||||
l = mid;
|
||||
else
|
||||
r = mid;
|
||||
}
|
||||
```
|
||||
|
||||
### 实数三分代码
|
||||
|
||||
```cpp
|
||||
double l = L, r = R;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
double lmid = l + (r - l) / 3, rmid = r - (r - l) / 3;
|
||||
if (f(lmid) < f(rmid))
|
||||
l = lmid;
|
||||
else
|
||||
r = rmid;
|
||||
}
|
||||
double top = f(l);
|
||||
```
|
||||
|
||||
### 二分三分例题
|
||||
|
||||
#### CF1355E Restorer Distance
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF1355E)
|
||||
|
||||
给定 $n$ 个墙的高度,你可以执行以下操作:
|
||||
|
||||
- 花费 $A$ 的代价往某列加入一个砖块
|
||||
- 花费 $R$ 的代价往某列去除一个砖块
|
||||
- 话费 $M$ 的代价往从某列拿出一个砖块放到另一列
|
||||
|
||||
求需要多少钱使每一列高度相等
|
||||
|
||||
##### CF1355E题解
|
||||
|
||||
考虑固定最后每列砖块数量 $H$。
|
||||
|
||||
记 $P=\sum_imax\{H - h_i, 0\}, Q=\sum_imax\{h_i - H, 0\}$
|
||||
|
||||
判断 $M$ 和 $A + R$ 的数量关系,若 $M \lt A + R$,则执行 $min(P, Q)$ 次操作三,$abs(P - Q)$ 次操作一或二。
|
||||
|
||||
则此时的总代价为
|
||||
|
||||
$\left\{
|
||||
\begin{aligned}
|
||||
&AP + (M - A)Q &(P \ge Q)\\
|
||||
&(M - R)P + RQ &(P \lt Q)
|
||||
\end{aligned}
|
||||
\right.
|
||||
\ (M=min\{M, A + R\})
|
||||
$
|
||||
|
||||
此时考虑用 $f(H)$ 表示高度为 $H$ 时的总代价,则 $f$ 为一个下凸函数。
|
||||
|
||||
三分 $f$ 寻找其最小值即可。
|
||||
|
||||
##### CF1355E参考代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
#define int long long
|
||||
using namespace std;
|
||||
int N, A, R, M;
|
||||
const int maxN = 1e5 + 5;
|
||||
int h[maxN], cxy[maxN];
|
||||
int calc(int H)
|
||||
{
|
||||
int p = upper_bound(h + 1, h + N + 1, H) - h - 1, X = p * H - cxy[p], Y = (cxy[N] - cxy[p]) - (N - p) * H, c = min(X, Y);
|
||||
return A * (X - c) + R * (Y - c) + M * c;
|
||||
}
|
||||
bool check(int lmid, int rmid) { return calc(lmid) < calc(rmid); }
|
||||
signed main()
|
||||
{
|
||||
#ifndef ONLINE_JUDGE
|
||||
freopen("3.in", "r", stdin);
|
||||
#endif
|
||||
scanf("%lld%lld%lld%lld", &N, &A, &R, &M);
|
||||
M = min(M, A + R);
|
||||
for (int i = 1; i <= N; ++i)
|
||||
scanf("%lld", &h[i]);
|
||||
sort(h + 1, h + N + 1);
|
||||
for (int i = 1; i <= N; ++i)
|
||||
cxy[i] = cxy[i - 1] + h[i];
|
||||
int l = h[1], r = h[N];
|
||||
while (l < r)
|
||||
{
|
||||
int lmid = l + (r - l) / 3, rmid = r - (r - l) / 3;
|
||||
if (check(lmid, rmid))
|
||||
r = rmid - 1;
|
||||
else
|
||||
l = lmid + 1;
|
||||
}
|
||||
printf("%lld\n", calc(l));
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 分治
|
||||
|
||||
### 分治概括
|
||||
|
||||
如图
|
||||
|
||||

|
||||
|
||||
### 分治应用
|
||||
|
||||
#### 分治求逆序对
|
||||
|
||||
考虑分治 $[l, mid]$ 与 $[mid+1, r]$ 中的逆序对,再考虑跨越 $mid$ 的逆序对。
|
||||
|
||||
### 分治例题
|
||||
|
||||
#### CF436D Pudding Monsters
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF436D)
|
||||
|
||||
在 $n \times n$ 的棋盘上有几枚棋子,其中第 $i$ 枚棋子位于第 $i$ 行第 $y$ 列,保证每行每列恰有一枚棋子。现在你打算制作一个大小为 $k$ 的正方形外框( $k$ 由你在 $1 - n$ 的整数中指定)并框住 $k \times k$ 的子棋盘,使得其内共含枚棋子。求有多少种框法。
|
||||
|
||||
##### CF436D题解
|
||||
|
||||
记 $a_i$ 表示第 $i$ 行棋子所在的列。
|
||||
|
||||
所以 $a_{1...n}$ 形成长度为 $n$ 的排列
|
||||
|
||||
考虑第 $i$ 行到第 $j$ 行的框法
|
||||
|
||||
$max\{a_i, ..., a_j\}-min\{a_i, ..., a_j\} = i-j$,否则无法框全。
|
||||
|
||||
于是问题从二维降至一维,即统计排列 $a$ 中有多少满足上式的 $[i, j]$ 子段。
|
||||
|
||||
考虑每次递归预处理以下数组:
|
||||
|
||||
- $sufmax[i] = max(a_i, a_{i+1}, ..., a_{mid})$
|
||||
- $premax[j] = max(a_{mid+1}, ..., a_{j-1}, a_j)$
|
||||
- $sufmin[i] = min(a_i, a_{i+1}, ..., a_{mid})$
|
||||
- $premin[j] = min(a_{mid+1}, ..., a_{j-1}, a_j)$
|
||||
|
||||
原式即可被化简成:
|
||||
|
||||
$max\{sufmax[i], premax[j]\} - min\{sufmin[i], premin[j]\}=j - i$
|
||||
|
||||
接下来考虑 $max, min$ 的选取,共 $4$ 种。
|
||||
|
||||
- $max, min$ 取同侧
|
||||
|
||||
以都取左侧为例 $(sufmax, sufmin)$
|
||||
|
||||
在 $[l, mid]$ 中枚举 $i$,由等式, $j$ 是唯一确定的,所以只需要验证以下两条式子:
|
||||
|
||||
$\left\{
|
||||
\begin{aligned}
|
||||
&sufmax[i] \gt premax[j]\\
|
||||
&sufmin[i] \lt premin[j]
|
||||
\end{aligned}
|
||||
\right.
|
||||
$
|
||||
|
||||
- $max, min$ 异侧
|
||||
|
||||
以 $max$ 取左, $min$ 取右为例 $(sufmax, premin)$
|
||||
|
||||
$\left\{
|
||||
\begin{aligned}
|
||||
&i + surmax[i] = j + premin[j]\\
|
||||
&sufmax[i] \gt premax[j]\\
|
||||
&sufmin[i] \ge premin[j]
|
||||
\end{aligned}
|
||||
\right.
|
||||
$
|
||||
|
||||
从大到小枚举 $i$,发现 $j$ 是一个滑动的范围,可以用[双指针](#双指针)维护。
|
||||
|
||||
--- 待完善 ---
|
||||
|
||||
---
|
||||
|
||||
## 搜索进阶
|
||||
|
||||
## 搜索
|
||||
|
||||
比赛中最暴力的方案,往往是想不出正解时用来骗部分分用的。
|
||||
|
||||
当然,如果剪枝够牛逼,可能也能获得不错的分数。
|
||||
|
||||
### 带剪枝的搜索
|
||||
|
||||
剪枝,指搜索过程中若遇到已经不可能成为答案的分支,则返回,防止不必要的搜索。
|
||||
|
||||
### 启发式搜索
|
||||
|
||||
更高级的剪枝,对当前搜索状态进行估价,如果明显不是最优解则返回.
|
||||
|
||||
### 迭代加深搜索
|
||||
|
||||
在搜索开始指定搜索深度 $D$。若范围内无合法解,则扩大 $D$ 再次搜索。
|
||||
|
||||
啊对结束了。jpg
|
||||
|
||||
---
|
||||
|
||||
## 倍增
|
||||
|
||||
### RMQ与ST表
|
||||
|
||||
RMQ 是Range Maximum/Minimum Query的缩写,表示区间最大(最小)值。
|
||||
|
||||
而 ST 表适用于解决**可重复贡献问题**的数据结构。
|
||||
|
||||
何为**可重复贡献问题**?记 $f(l, r)$ 表示 $[l, r]$ 这个区间的答案,可重复贡献问题就是,对于所有 $R \ge L$,$f(l, r)$ 可以被记为 $f(l, R)$ 与 $f(L, r)$ 的和合并。也就是说,可以把大区间的答案拆分为**可重合的**小区间的答案合并。
|
||||
|
||||
Obviously,区间 max,区间 and,区间 gcd 都是可重复贡献问题,可以用 ST 表维护优化。但是区间加、区间积就不是,因为不可以加上或者乘上一个已经运算过的数。
|
||||
|
||||
#### RMQ与ST表应用
|
||||
|
||||
##### ST表求区间最大值
|
||||
|
||||
给定 $n$ 个数,有 $m$ 次询问,对于每次询问,需要输出 $[l, r]$ 中的最大值。
|
||||
|
||||
令 $f(i, j)$ 表示区间 $[i, i + 2 ^ i - 1]$ 中的最大值。显然 $f(i, 0) = a_i$
|
||||
|
||||
很容易写出转移: $f(i, j) = max(f(i, j - 1), f(i + 2 ^ j - 1, j - 1))$
|
||||
|
||||
对于每个询问 $[l, r]$,我们把它分成两部分 $f(l, l + 2 ^ s - 1)$ 和 $f(r - 2 ^ s + 1, r)$,其中 $s = \lfloor log_2(r - l + 1) \rfloor$,两部分的最大值即为答案。
|
||||
|
||||
注意: $log$ 要预处理, $log_2 i = log_2 \frac{i}{2} + 1$。
|
||||
|
||||
#### ST表的优劣
|
||||
|
||||
ST表能够较好的维护可重复贡献区间问题,时间复杂度较低,但是维护的信息有限,不支持扩展,修改。
|
||||
|
||||
### 倍增例题
|
||||
|
||||
#### P2048 NOI2010 超级钢琴
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P2048)
|
||||
|
||||
给定一个长度为 $n$ 的序列,要求选 $k$ 段长度在 $L$ 到 $R$ 之间的区间,使得 $k$ 个区间的区间和最大(规定区间的值为区间和)。区间可以相交或包含,但是一个区间只能选一次。
|
||||
|
||||
##### P2048暴力代码
|
||||
|
||||
```cpp
|
||||
// 预期得分20
|
||||
#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
const int maxn = 5 * 1e5 + 10;
|
||||
int n, k, L, R, ans;
|
||||
int sum[maxn];
|
||||
priority_queue<int> q;
|
||||
int main()
|
||||
{
|
||||
scanf("%d%d%d%d", &n, &k, &L, &R);
|
||||
for (int i = 1; i <= n; i++)
|
||||
scanf("%d", &sum[i]), sum[i] += sum[i - 1];
|
||||
for (int i = 1; i <= n; i++)
|
||||
for (int j = i; j <= n; j++)
|
||||
if (j - i + 1 >= L && j - i + 1 <= R)
|
||||
q.push(sum[j] - sum[i - 1]);
|
||||
for (int i = 1; i <= k; i++)
|
||||
ans += q.top(), q.pop();
|
||||
printf("%d", ans);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
##### P2048题解
|
||||
|
||||
首先,考虑将区间和转换为前缀和之差。
|
||||
|
||||
对于一个固定的 $l$,用ST表可以查询 $r \in [R_1, R_2]$ 的前缀和最大的 $r$,此时 $[l, r]$ 即为该条件下最大的。
|
||||
|
||||
然后将每个左端点的最优解放入优先队列,每次取出优先队列中的最优解,然后再去查询 $[l, r]$ 区间中去除最优解的最优解放入优先队列,取 $k$ 次即可
|
||||
|
||||
#### 例题2
|
||||
|
||||
有一张 $n$ 个点, $m$ 条边的有向图,边权为 $1$。从起点出发,每次能走恰好 $2^k$ 的旅程, $k$ 为自然数。求问最少多少次才能达到终点。
|
||||
|
||||
$n \le 50, m \le 2500$
|
||||
|
||||
##### 例题2题解
|
||||
|
||||
令 $g(u, v, j)$ 表示从点 $u$ 到点 $v$ 是否存在长度为 $2 ^ j$ 的路径, $G(u, v)$ 表示 $u, v$ 能否一步到达。
|
||||
|
||||
转移如下:
|
||||
|
||||
$g(u, w, j) = g(u, v, j - 1) \&\& g(v, w, j - 1), G(u, v)=g(u, v, 0)||g(u, v, 1)||...||g(u, v, n - 1)$
|
||||
|
||||
因为可能有环,所以 $j$ 需要枚举到 $n - 1$ 才能结束。
|
||||
|
||||
这样子相当于是重建了一张新图,因此在新图上BFS求最短路即可。
|
||||
|
||||
#### P1084 疫情控制
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P1084)
|
||||
|
||||
H国有 $n$ 个城市,这 $n$ 个城市用 $n - 1$ 条双向道路连接构成一棵树。 $1$ 号城市是首都,也是树中的根节点。
|
||||
|
||||
根节点爆发了传染病,需要在一些节点建立检查点(根节点除外),使得根节点到每一个叶节点的路径上都有检查点。
|
||||
|
||||
一些节点中驻扎有军队,一个城市可以有多个军队。每一支军队可以在城市间移动,且不同军队可以同时移动,并在根节点外的任意一个节点建立检查点。一支军队移动的时间等于路径长度。
|
||||
|
||||
##### P1084题解
|
||||
|
||||
性质:在根的**同一棵**(指在不考虑走进根的其他子树的情况下)子树内的军队,越靠近根节点越优。
|
||||
|
||||
考虑二分答案,让所有军队的行动时间不超过 $mid$。
|
||||
|
||||
预处理每一个节点的祖先的倍增数组,然后利用倍增快速上移军队,如果能达到根节点并且还有剩余时间,则去考虑其他子树,该方法保留备用。
|
||||
|
||||
DFS查询所有叶子节点,得到分界点哪些子树需要军队驻扎,最后贪心完成匹配
|
||||
|
||||
复杂度 $O(n log n log t)$
|
||||
|
||||
#### P3295 萌萌哒
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P3295)
|
||||
|
||||
有一个 $n$ 位的十进制数 $a$(保证无前导0),给出 $m$ 条限制,每条限制的格式如 $(l_1, r_1, l_2, r_2)$ (保证 $r_1 - l_1 = r_2 - l_2$),表示这个数的第 $[l_1, r_1]$ 位与第 $[l_2, r_2]$ 位对应相同。问有多少个这样的数满足条件。输出答案 $mod\ 10 ^ 9 + 7。
|
||||
|
||||
$1 \le n, m \le 10 ^ 5$
|
||||
|
||||
##### P3295朴素做法
|
||||
|
||||
考虑用[并查集](../数据结构/数据结构.md#并查集)维护,将对应位置上的点依次合并。
|
||||
|
||||
设最后有 $cnt$ 个连通块,答案即为 $9 \times 10 ^ {cnt - 1}$
|
||||
|
||||
复杂度为 $O(nm \alpha(n))$,无法通过。
|
||||
|
||||
##### P3295正解
|
||||
|
||||
考虑用[ST表](#rmq与st表)的方式优化合并。
|
||||
|
||||
具体来说,开 $log_2 n$ 个并查集,第 $k$ 个表示对于并查集内任意两点 $u, v$,$u = v, u + 1 = v + 1, ..., u + 2 ^ k - 1 = v + 2 ^ k - 1$
|
||||
|
||||
对于每次给定的合并操作,我们进行多次 $2 ^ k$ 的合并。
|
||||
|
||||
复杂度 $O((n + q) log n \alpha(n))$
|
||||
|
||||
---
|
||||
|
||||
## 双指针
|
||||
|
||||
顾名思义,就是使用两个指针,在序列链表上指向位置,树、图上指向节点,通过两点的同向或者反向移动来维护、统计信息。
|
||||
|
||||
双指针的基本思路:先两重循环,再使用双指针优化掉一重循环。
|
||||
|
||||
### 双指针例题
|
||||
|
||||
#### 双指针例题1
|
||||
|
||||
给定长度为 $n$ 的序列 $a$ 和整数 $k$,求 $cnt_{1 \le x \lt y \le n, a_x + a_y = k}$
|
||||
|
||||
$n \le 10^5, k \le 10^9$
|
||||
|
||||
##### 双指针例题1题解
|
||||
|
||||
首先肯定想到两层循环暴力,时间复杂度 $O(n^2)$
|
||||
|
||||
然后考虑使用双指针来优化掉一层循环。对数组进行排序,然后从前往后枚举第一个数,从后往前枚举第二个数。
|
||||
|
||||
容易发现,当枚举到一组满足条件的解时,第一个数增大,第二个数肯定需要减小。
|
||||
|
||||
##### 双指针例题1代码
|
||||
|
||||
```cpp
|
||||
sort(a + 1, a + n + 1);
|
||||
int res = 0;
|
||||
for (int l = 1, r = n; l < r; ++l)
|
||||
{
|
||||
while (a[l] + a[r] > k && r > l + 1)
|
||||
--r;
|
||||
if (a[l] + a[r] == k)
|
||||
++res;
|
||||
}
|
||||
```
|
||||
|
||||
#### 双指针例题2
|
||||
|
||||
给定一个长度为 $n$ 的序列 $a$,找出最长的一个连续的子序列,使得其没有重复元素。
|
||||
|
||||
$n \le 10^5, |a_i| \le 10^9$
|
||||
|
||||
##### 双指针例题2题解
|
||||
|
||||
首先想到两重循环+set 进行维护,时间复杂度 $O(n^2 \log n)$
|
||||
|
||||
然后考虑双指针优化。容易发现可以在开始时设 $l = r = 1$,每次将尾指针向右移动一位,若这个元素在 set 内出现过,则将首指针向右移动并删除 set 中的这个元素,直到尾指针的元素不在 set 内再开始移动尾指针,最终答案即为最大的 $l - r + 1$。时间复杂度 $O(n \log n)$
|
||||
|
||||
##### 双指针例题2代码
|
||||
|
||||
```cpp
|
||||
set<int> S;
|
||||
int l = 1, r = 1, ans1 = -1, ansr = -1, len = 0;
|
||||
while (l <= n)
|
||||
{
|
||||
while (r <= n)
|
||||
{
|
||||
if (S.find(a[r]) == S.end())
|
||||
{
|
||||
S.insert(a[r++]);
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (len < r - 1)
|
||||
{
|
||||
len = r - 1, ans1 = l, ansr = r - 1;
|
||||
}
|
||||
S.erase(S.find(a[l++]));
|
||||
}
|
||||
```
|
||||
|
||||
#### P1381 单词背诵
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P1381)
|
||||
|
||||
有 $n$ 的单词需要背诵,我们需要使用文章中的一段来记住这些单词。
|
||||
|
||||
每篇文章由 $m$ 个单词组成
|
||||
|
||||
##### P1381题解
|
||||
|
||||
##### P1381代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
const int maxn = 100010;
|
||||
int n, m, h, t, n1, nown1, j_i = INT_MAX; // n1表示最多单词 j_i表示最小段落
|
||||
map<string, int> mapp;
|
||||
string s;
|
||||
int a[maxn], b[maxn], q[maxn];
|
||||
bool recited[maxn];
|
||||
int main()
|
||||
{
|
||||
h = 1;
|
||||
t = 0;
|
||||
scanf("%d", &n);
|
||||
for (int i = 1; i <= n; ++i)
|
||||
{
|
||||
cin >> s;
|
||||
mapp[s] = i;
|
||||
}
|
||||
scanf("%d", &m);
|
||||
for (int i = 1; i <= m; ++i)
|
||||
{
|
||||
cin >> s;
|
||||
a[i] = mapp[s];
|
||||
if (a[i] == 0)
|
||||
continue;
|
||||
if (!recited[a[i]])
|
||||
{
|
||||
++n1;
|
||||
recited[a[i]] = true;
|
||||
}
|
||||
}
|
||||
for (int i = 1; i <= m; ++i)
|
||||
{
|
||||
if (b[a[i]] == 0 && a[i] != 0)
|
||||
++nown1;
|
||||
b[a[i]] = i;
|
||||
q[++t] = i;
|
||||
while (q[h] < b[a[q[h]]] && h <= t)
|
||||
++h;
|
||||
if (nown1 == n1)
|
||||
j_i = min(j_i, i - q[h] + 1);
|
||||
}
|
||||
if (nown1 == 0)
|
||||
printf("0\n0\n");
|
||||
else
|
||||
printf("%d\n%d\n", n1, j_i);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 折半搜索
|
||||
|
||||
---
|
||||
|
||||
## 哈希,KMP
|
||||
|
||||
---
|
||||
|
||||
## 贪心
|
||||
|
||||
---
|
||||
|
||||
## 构造转换模拟
|
||||
|
||||
### CF1916D Mathematical Problem
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF1916D)
|
||||
|
||||
给定奇数 $n$,求出 $n$ 个正整数满足:
|
||||
|
||||
- 都是完全平方数。
|
||||
- 长度为 $n$ 且没有前导 $0$。
|
||||
- 组成这 $n$ 个数的数字( $[0,9]$ 内数字)组成的可重集相同。
|
||||
|
||||
输出任意一种方案。
|
||||
|
||||
#### CF1916D题解
|
||||
|
||||
打表观察发现:
|
||||
|
||||
```txt
|
||||
13: 169
|
||||
14: 196 n=3
|
||||
31: 961
|
||||
|
||||
103: 10609 130: 16900
|
||||
140: 19600 n=5
|
||||
301: 90601 310: 96100
|
||||
|
||||
1003: 1006009 1030: 1060900 1300: 1690000
|
||||
1400: 196000 n=7
|
||||
3001: 9006001 3010: 9060100 3100: 9610000
|
||||
```
|
||||
|
||||
在输入为 $n$ 的情况下,只需要每次把 $n-3$ 个 $0$ 分别分配到 $14$ 的后面及 $13,31$ 的后面和中间即可。
|
||||
|
||||
#### CF1916D参考代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
int n;
|
||||
void work(char s[])
|
||||
{
|
||||
printf("%s",s);
|
||||
for (int i = 1; i <= n - 3; ++i)
|
||||
printf("0");
|
||||
printf("\n");
|
||||
}
|
||||
int main()
|
||||
{
|
||||
#ifndef ONLINE_JUDGE
|
||||
freopen("2.in", "r", stdin);
|
||||
#endif
|
||||
int t;
|
||||
scanf("%d", &t);
|
||||
while (t--)
|
||||
{
|
||||
scanf("%d", &n);
|
||||
if (n == 1)
|
||||
{
|
||||
puts("1");
|
||||
continue;
|
||||
}
|
||||
work("169");
|
||||
work("196");
|
||||
work("961");
|
||||
for (int i = 1; i < n / 2; ++i) //
|
||||
{
|
||||
printf("1");
|
||||
for (int j = 1; j <= i; ++j)
|
||||
printf("0");
|
||||
printf("6");
|
||||
for (int j = 1; j <= i; ++j)
|
||||
printf("0");
|
||||
printf("9");
|
||||
for (int j = 3 + 2 * i; j < n; ++j)
|
||||
printf("0");
|
||||
printf("\n");
|
||||
|
||||
printf("9");
|
||||
for (int j = 1; j <= i; ++j)
|
||||
printf("0");
|
||||
printf("6");
|
||||
for (int j = 1; j <= i; ++j)
|
||||
printf("0");
|
||||
printf("1");
|
||||
for (int j = 3 + 2 * i; j < n; ++j)
|
||||
printf("0");
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### CF1912E Evaluate It and Back Again
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF1912E)
|
||||
|
||||
给定两个数,要求你写出一个只包含数字, $+, \times, -$ 的式子,正着看是第一个数,反着看是第二个数。
|
||||
|
||||
#### CF1912E题解
|
||||
|
||||
观察发现只有**正一位数**和 $+, \times$ 的**单项式**倒过来读还是一样的,由 $-0$ 结尾的单项式倒过来读和原式结果相反。
|
||||
|
||||
发现可以将一个要求倒着读后数值不变的数表示为 $9$ 进制的形式,这样多项式中每一项都是倒着读后不变的。
|
||||
|
||||
- 对于两个奇偶性相同的数,可以求两个数的平均数,将 $p, q$ 分别表示为 $p = \overline{pq} + \frac{p - q}{2}, q = \overline{pq} - \frac{p - q}{2}$
|
||||
- 对于两个奇偶性不同的数,也可以求平均数,但是为了让 $p, q$ 奇偶性统一,需要给 $p, q$ 分别减去一个正着读是奇数,反着读是偶数的数,如 $12$,然后即可按照第一种形式做
|
||||
|
||||
然后就是注意特判两个相同的数和 `0 0` 的情况,别忘记开long long就结束了
|
||||
|
||||
#### CF1912E参考代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
string ans;
|
||||
string to_9(long long n) //转换为正反均相同的多项式
|
||||
{
|
||||
bool flag = n >= 0;
|
||||
if (!flag)
|
||||
n = -n;
|
||||
ans = "";
|
||||
long long cnt = 0;
|
||||
long long yu[20] = {};
|
||||
while (n >= 10ll)
|
||||
yu[cnt++] = n % 9ll, n /= 9ll;
|
||||
yu[cnt] = n;
|
||||
if (!flag)
|
||||
ans += "0-";
|
||||
for (long long i = 0; i <= cnt; ++i)
|
||||
{
|
||||
if (yu[cnt - i] != 0)
|
||||
{
|
||||
for (long long j = 0; j < cnt - i; ++j)
|
||||
ans += "9*";
|
||||
ans += yu[cnt - i] + '0';
|
||||
if (flag)
|
||||
ans += "+0+";
|
||||
else
|
||||
ans += "-0-";
|
||||
}
|
||||
}
|
||||
while (ans[ans.size() - 1] == '+' || ans[ans.size() - 1] == '-')
|
||||
ans.pop_back(); // 去掉多于符号
|
||||
return ans;
|
||||
}
|
||||
string to_9_0(long long n) // 转换为正反读相反的多项式
|
||||
{
|
||||
bool flag = n >= 0;
|
||||
if (!flag)
|
||||
n = -n;
|
||||
ans = "";
|
||||
long long cnt = 0;
|
||||
long long yu[20] = {};
|
||||
while (n >= 10)
|
||||
{
|
||||
yu[cnt++] = n % 9ll;
|
||||
n /= 9ll;
|
||||
}
|
||||
yu[cnt] = n;
|
||||
if (!flag)
|
||||
ans += "0-";
|
||||
for (long long i = 0; i <= cnt; ++i)
|
||||
{
|
||||
if (yu[cnt - i] != 0)
|
||||
{
|
||||
for (long long j = 0; j < cnt - i; ++j)
|
||||
ans += "9*";
|
||||
ans += yu[cnt - i] + '0';
|
||||
if (flag)
|
||||
ans += "-0+";
|
||||
else
|
||||
ans += "+0-";
|
||||
}
|
||||
}
|
||||
while (ans[ans.size() - 1] == '+' || ans[ans.size() - 1] == '-')
|
||||
ans.pop_back(); // 去掉多于符号
|
||||
return ans;
|
||||
}
|
||||
int main()
|
||||
{
|
||||
long long p = 0, q = 0;
|
||||
scanf("%lld%lld", &p, &q);
|
||||
if (abs(p) % 2 == abs(q) % 2) // 同奇偶
|
||||
{
|
||||
long long _pq = p + q >> 1, p_q = p - _pq;
|
||||
if (p == 0 && q == 0) // 特判
|
||||
{
|
||||
puts("0");
|
||||
return 0;
|
||||
}
|
||||
cout << to_9(_pq) << ((_pq != 0 && p_q != 0) ? "+" : "") << to_9_0(p_q) << endl;
|
||||
}
|
||||
else // 异奇偶
|
||||
{
|
||||
long long _pq = (p - 21 + q - 12) >> 1, p_q = p - 21 - _pq; // 转换为同奇偶
|
||||
cout << to_9(_pq) << ((_pq != 0 && p_q != 0) ? "+" : "") << to_9_0(p_q) << ((p_q != 0 || _pq != 0) ? "+" : "") << "21" << endl;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### CF739A Alyona and mex
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF739A)
|
||||
|
||||
给定 $m$ 个区间,构造出一个长度为 $n$ 的序列,使得这 $m$ 个区间的最小 $mex$ 最大。 $mex$ 定义为最小的没有出现过的自然数。
|
||||
|
||||
#### CF739A题解
|
||||
|
||||
观察样例发现两次输出的 $mex$ 均等于最短的区间长度,显然这不是巧合。
|
||||
|
||||
首先很容易得出 $mex(S) \le \lvert S \rvert$,且当 $S$ 是 $0$ 到 $\lvert S \rvert - 1$ 时取等,所以最终答案不会超过最短的区间的长度,然后去思考如何构造出答案等于最短区间长度的情况。
|
||||
|
||||
求出最短的区间长度为 $x$,则只要在数组中循环填入 $0, 1, 2, ..., x - 1$,求能保证每一个长度大于等于 $x$ 的区间 $mex$ 一定都等于 $x$,因为每一个长度大于等于 $x$ 的区间都至少覆盖一次 $0, 1, 2, ..., x - 1$。(可以自己手搓几个样例试试)
|
||||
|
||||
#### CF739A代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
int main()
|
||||
{
|
||||
int n, m;
|
||||
scanf("%d%d", &n, &m);
|
||||
int minlen = INT_MAX;
|
||||
for (int i = 0; i < m; ++i)
|
||||
{
|
||||
int l, r;
|
||||
scanf("%d%d", &l, &r);
|
||||
minlen = min(minlen, r - l + 1);
|
||||
}
|
||||
printf("%d\n", minlen);
|
||||
int cnt = 0;
|
||||
for (int i = 1; i <= n; ++i)
|
||||
printf("%d ", i % minlen);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### CF1495C Garden of the Sun
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF1495C)
|
||||
|
||||
给定一个 $n \times m$ 的矩阵,由 `X` 和 `.` 组成,保证给出的矩阵的所有 `X` 之间没有公共点或公共边。
|
||||
|
||||
请把一些 `.` 换成 `X`,使得所有 `X` 四连通且不存在简单环
|
||||
|
||||
如果有多个解,输出任意即可
|
||||
|
||||
$n, m \le 500, \sum n \times m \le 250000$
|
||||
|
||||
#### CF1495C题解
|
||||
|
||||
考虑将整个矩阵分割成三行三行的形式,因为题目保证没有公共边,所以就可以先把中间行填满 `X`,然后再逐个考虑每个三行之间的连接。
|
||||
|
||||
举例:
|
||||
|
||||
```txt
|
||||
.X...
|
||||
....X
|
||||
.X...
|
||||
.....
|
||||
X.X.X
|
||||
```
|
||||
|
||||
```txt
|
||||
.X...
|
||||
XXXXX 中间行填满
|
||||
.X...
|
||||
-----分割
|
||||
.....
|
||||
XXXXX 中间行填满
|
||||
```
|
||||
|
||||
```txt
|
||||
.X...
|
||||
XXXXX
|
||||
.X...
|
||||
.X... 连接
|
||||
XXXXX
|
||||
```
|
||||
|
||||
#### CF1495C代码
|
||||
|
||||
```cpp
|
||||
```
|
||||
|
||||
---
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,215 @@
|
||||
# 数据结构
|
||||
|
||||
- [数据结构](#数据结构)
|
||||
- [单调队列](#单调队列)
|
||||
- [单调队列引入](#单调队列引入)
|
||||
- [引入问题滑动窗口](#引入问题滑动窗口)
|
||||
- [单调队列例题](#单调队列例题)
|
||||
- [P1714 切蛋糕](#p1714-切蛋糕)
|
||||
- [P1714题解](#p1714题解)
|
||||
- [单调栈](#单调栈)
|
||||
- [并查集](#并查集)
|
||||
- [并查集基础](#并查集基础)
|
||||
- [并查集查询](#并查集查询)
|
||||
- [并查集合并](#并查集合并)
|
||||
- [带权并查集](#带权并查集)
|
||||
- [带权并查集例题](#带权并查集例题)
|
||||
- [P2024 NOI2001食物链](#p2024-noi2001食物链)
|
||||
- [P2024题解](#p2024题解)
|
||||
- [可撤销并查集](#可撤销并查集)
|
||||
- [并查集例题](#并查集例题)
|
||||
- [并查集例题1](#并查集例题1)
|
||||
- [并查集例题1题解](#并查集例题1题解)
|
||||
- [并查集例题2](#并查集例题2)
|
||||
- [并查集例题2题解](#并查集例题2题解)
|
||||
- [并查集例题3](#并查集例题3)
|
||||
- [并查集例题3题解](#并查集例题3题解)
|
||||
- [堆](#堆)
|
||||
- [线段树](#线段树)
|
||||
- [树状数组](#树状数组)
|
||||
- [Trie](#trie)
|
||||
|
||||
## 单调队列
|
||||
|
||||
### 单调队列引入
|
||||
|
||||
#### 引入问题滑动窗口
|
||||
|
||||
> 如果有人比你小还比你强,那你就没有任何用了 —— noip
|
||||
|
||||
有 $n$ 个 oier ,每个 oier 有成绩和考试,保证给定数据中 oier 的出生年份单调,求每 $k$ 个年份时这 $n$ 个 oier 的最大成绩。
|
||||
|
||||
伪代码:
|
||||
|
||||
```cpp
|
||||
deque<pair<int,int>> q; // 成绩,年份
|
||||
while(!q.empty() && newgrade>q.back()) q.pop_back;
|
||||
q.push_back(newgrade,newyear);
|
||||
while(q.front().second要退役){
|
||||
q.pop_front();
|
||||
}
|
||||
|
||||
最大值q.front().first
|
||||
```
|
||||
|
||||
时间复杂度 $O(n)$ (均摊复杂度,可能某次while执行很久,可能某次很快)
|
||||
|
||||
### 单调队列例题
|
||||
|
||||
#### P1714 切蛋糕
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P1714)
|
||||
|
||||
有一个长方形的蛋糕,被不同的色彩分成了 $N$ 个相同的小块,每个小块有着对应的幸运值。
|
||||
|
||||
小 Z 作为寿星,希望吃到的蛋糕的幸运值最大,但是他最多只能吃 $M (M \le N)$
|
||||
|
||||
你需要在这 $N$ 小块蛋糕中找出连续的几块,使得其上总幸运值最大。
|
||||
|
||||
形式化地,在数列 ${p_n}$ 中,找出一个长度小于 $M$ 的子段 $[l, r]$,最大化 $\sum^{r}_{i - l} {p_i}$
|
||||
|
||||
$1 \le n \le 5 \times 10^5$
|
||||
|
||||
##### P1714题解
|
||||
|
||||
---
|
||||
|
||||
## 单调栈
|
||||
|
||||
---
|
||||
|
||||
## 并查集
|
||||
|
||||
### 并查集基础
|
||||
|
||||
基础操作:合并、查询
|
||||
|
||||
#### 并查集查询
|
||||
|
||||
对于给定的 $a$,沿着树向上移动知道根节点即可
|
||||
|
||||
在合并的过程中的每个节点都属于这个集合,所以将父亲节点直接设为根即可。
|
||||
|
||||
#### 并查集合并
|
||||
|
||||
合并两个集合时,可以将一个集合的根节点设为另一个集合的根节点。
|
||||
|
||||
为了防止退化,可以将节点较少或深度较小的一棵树连接到令一颗上。
|
||||
|
||||
### 带权并查集
|
||||
|
||||
我们可以在并查集的边上维护某种值,在路径压缩时产生运算。
|
||||
|
||||
#### 带权并查集例题
|
||||
|
||||
##### P2024 NOI2001食物链
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P2024)
|
||||
|
||||
动物王国中有三类动物 $A,B,C$,这三类动物的食物链构成了有趣的环形。$A$ 吃 $B$,$B$ 吃 $C$,$C$ 吃 $A$。
|
||||
|
||||
现有 $N$ 个动物,以 $1 \sim N$ 编号。每个动物都是 $A,B,C$ 中的一种,但是我们并不知道它到底是哪一种。
|
||||
|
||||
有人用两种说法对这 $N$ 个动物所构成的食物链关系进行描述:
|
||||
|
||||
- 第一种说法是 `1 X Y`,表示 $X$ 和 $Y$ 是同类。
|
||||
- 第二种说法是`2 X Y`,表示 $X$ 吃 $Y$。
|
||||
|
||||
此人对 $N$ 个动物,用上述两种说法,一句接一句地说出 $K$ 句话,这 $K$ 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
|
||||
|
||||
- 当前的话与前面的某些真的话冲突,就是假话;
|
||||
- 当前的话中 $X$ 或 $Y$ 比 $N$ 大,就是假话;
|
||||
- 当前的话表示 $X$ 吃 $X$,就是假话。
|
||||
|
||||
你的任务是根据给定的 $N$ 和 $K$ 句话,输出假话的总数。
|
||||
|
||||
###### P2024题解
|
||||
|
||||
考虑维护带权并查集,对每次给定的话进行查询与合并。
|
||||
|
||||
具体的,对于并查集中的每两个节点,使用 $0$ 表示互为同类, $1$ 表示父亲吃儿子, $2$ 表示儿子吃父亲。
|
||||
|
||||
对于每句话 `1/2 X Y`,查询 $X, Y$ 之间的关系,若无冲突便合并即可,注意路径压缩时的运算。
|
||||
|
||||

|
||||
|
||||
### 可撤销并查集
|
||||
|
||||
对于一开始相互独立的 $n$ 个点,维护以下两种操作共 $q$ 次:
|
||||
|
||||
1. 给定 $a, b$ 两个点,判断是否在同一个联通块中,如不在则在 $a, b$ 间连边。
|
||||
2. 撤销最后的若干次操作 $1$
|
||||
|
||||
要求复杂度 $O(q \log n)$
|
||||
|
||||
考虑用栈记录最近操作 $fa_x \leftarrow y$ ,每次撤销弹出栈顶,并将 $fa_x \leftarrow x$
|
||||
|
||||
因为要保证可逆,所以不能路径压缩,只能启发式合并。
|
||||
|
||||
### 并查集例题
|
||||
|
||||
#### 并查集例题1
|
||||
|
||||
有 $n$ 个点,初始时均为孤立点。
|
||||
|
||||
接下来有 $m$ 次连边操作,第 $i$ 次在 $a_i, b_i$ 之间连一条边。
|
||||
|
||||
接下来有 $q$ 次询问,每次询问两个点在第几次加边操作联通。
|
||||
|
||||
$1 \le n, m, q \le 10^5$
|
||||
|
||||
##### 并查集例题1题解
|
||||
|
||||
考虑对于每一次合并的 $a_i, b_i$,若两个点不联通,则在另外一颗树中连接 $a_i, b_i$,边权为 $i$。
|
||||
|
||||
每次查询就是在令一颗树中找到 $u, v$ 路径上边权最大值,可以使用树上倍增维护。
|
||||
|
||||
#### 并查集例题2
|
||||
|
||||
有一个长度为 $n$ 的 $01$ 序列 $a_1, \dots, a_n$,一开始全是 $0$。接下来进行如下 $m$ 次操作:
|
||||
|
||||
- $a_x \leftarrow 1$
|
||||
- 输出 $\min\{x \in [l,r], a_x = 0\}$
|
||||
|
||||
$1 \le n, m \le 10^6$
|
||||
|
||||
##### 并查集例题2题解
|
||||
|
||||
建立并查集,使得 $i$ 所在联通块的根节点为 $i$ 后面第一次出现 $0$ 的位置,初始 $f_i = i$
|
||||
|
||||
对于每一次 $a_x \leftarrow 1$,如果 $a_x$ 原先不为 $1$,则令 $f_x = f_{x - 1}$
|
||||
|
||||
每次查询输出 $x$ 的祖先。
|
||||
|
||||
#### 并查集例题3
|
||||
|
||||
给定一颗 $n$ 个节点的树,接下来有 $m$ 次操作:
|
||||
|
||||
- 加一条 $a_i$ 到 $b_i$ 的边。
|
||||
- 询问两个点之间是否存在至少两条**边不相交**的路径。
|
||||
|
||||
**边不相交:指的是两个路径没有公共边。**
|
||||
|
||||
$1 \le n, m \le 10^6$
|
||||
|
||||
##### 并查集例题3题解
|
||||
|
||||
前置知识:[强联通分量](../树上问题,图论/树上问题,图论.md#强联通分量)
|
||||
|
||||
---
|
||||
|
||||
## 堆
|
||||
|
||||
---
|
||||
|
||||
## 线段树
|
||||
|
||||
---
|
||||
|
||||
## 树状数组
|
||||
|
||||
---
|
||||
|
||||
## Trie
|
||||
|
||||
---
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,917 @@
|
||||
# 树上问题,图论
|
||||
|
||||
- [树上问题,图论](#树上问题图论)
|
||||
- [LCA](#lca)
|
||||
- [LCA定义](#lca定义)
|
||||
- [LCA基本性质](#lca基本性质)
|
||||
- [LCA算法流程](#lca算法流程)
|
||||
- [朴素LCA](#朴素lca)
|
||||
- [倍增LCA](#倍增lca)
|
||||
- [倍增LCA参考代码](#倍增lca参考代码)
|
||||
- [树链剖分](#树链剖分)
|
||||
- [树链剖分概念](#树链剖分概念)
|
||||
- [树链剖分基本定义](#树链剖分基本定义)
|
||||
- [树链剖分约定定义](#树链剖分约定定义)
|
||||
- [树链剖分基本性质](#树链剖分基本性质)
|
||||
- [树链剖分例题](#树链剖分例题)
|
||||
- [树链剖分例题0](#树链剖分例题0)
|
||||
- [树链剖分例题0题解](#树链剖分例题0题解)
|
||||
- [P1967 货车运输](#p1967-货车运输)
|
||||
- [P1967题解](#p1967题解)
|
||||
- [P1967示例代码](#p1967示例代码)
|
||||
- [最短路](#最短路)
|
||||
- [最短路算法](#最短路算法)
|
||||
- [Floyd](#floyd)
|
||||
- [Bellman–Ford](#bellmanford)
|
||||
- [SPFA](#spfa)
|
||||
- [Dijkstra](#dijkstra)
|
||||
- [最短路例题](#最短路例题)
|
||||
- [最小生成树](#最小生成树)
|
||||
- [最小生成树定义](#最小生成树定义)
|
||||
- [最小生成树算法](#最小生成树算法)
|
||||
- [Kruskal 算法](#kruskal-算法)
|
||||
- [Kruskal部分代码](#kruskal部分代码)
|
||||
- [Prim 算法](#prim-算法)
|
||||
- [Prim代码(来自OI Wiki)](#prim代码来自oi-wiki)
|
||||
- [Boruvka 算法](#boruvka-算法)
|
||||
- [Boruvka伪代码](#boruvka伪代码)
|
||||
- [延伸:次小生成树](#延伸次小生成树)
|
||||
- [非严格次小生成树](#非严格次小生成树)
|
||||
- [非严格次小生成树求法](#非严格次小生成树求法)
|
||||
- [严格次小生成树](#严格次小生成树)
|
||||
- [严格次小生成树求法](#严格次小生成树求法)
|
||||
- [延伸:瓶颈生成树](#延伸瓶颈生成树)
|
||||
- [瓶颈生成树性质](#瓶颈生成树性质)
|
||||
- [延伸:Kruskal 重构树](#延伸kruskal-重构树)
|
||||
- [Kruskal重构树性质](#kruskal重构树性质)
|
||||
- [最小生成树例题](#最小生成树例题)
|
||||
- [CF609E Minimum spanning tree for each edge](#cf609e-minimum-spanning-tree-for-each-edge)
|
||||
- [CF609E题解](#cf609e题解)
|
||||
- [CF609E代码](#cf609e代码)
|
||||
- [AT\_abc282\_e Choose Two and Eat One](#at_abc282_e-choose-two-and-eat-one)
|
||||
- [AT\_abc282\_e题解](#at_abc282_e题解)
|
||||
- [AT\_abc282\_e代码](#at_abc282_e代码)
|
||||
- [CF1857G Counting Graphs](#cf1857g-counting-graphs)
|
||||
- [CF1857G题解](#cf1857g题解)
|
||||
- [CF1857G代码](#cf1857g代码)
|
||||
- [P4768 归程](#p4768-归程)
|
||||
- [P4768题解](#p4768题解)
|
||||
- [P4768代码](#p4768代码)
|
||||
- [CF1706E Qpwoeirut and Vertices](#cf1706e-qpwoeirut-and-vertices)
|
||||
- [CF888G Xor-MST](#cf888g-xor-mst)
|
||||
- [CF1120D Power Tree](#cf1120d-power-tree)
|
||||
- [CF1120D题解](#cf1120d题解)
|
||||
- [强联通分量](#强联通分量)
|
||||
- [强联通分量概念](#强联通分量概念)
|
||||
- [Tarjan](#tarjan)
|
||||
- [Tarjan 前置概念](#tarjan-前置概念)
|
||||
|
||||
## LCA
|
||||
|
||||
### LCA定义
|
||||
|
||||
在一个**有根树**中,多个点(并非一定两个)离根最远的公共的祖先。
|
||||
|
||||
### LCA基本性质
|
||||
|
||||
1. $u$ 是 $v$ 的祖先,当且仅当 $LCA(u, v) = u$
|
||||
2. 如果 $u, v$ 互相不为祖先,那么 $u, v$ 分别处于 $LCA(u, v)$ 的两棵不同子树中
|
||||
3. 前序遍历中, $LCA(S)$ 出现在所有 $S$ 中的元素之前,后序遍历则相反。
|
||||
4. 两点集并的LCA为两点集分别的LCA的LCA,即 $LCA(A \cup B ) = LCA(LCA(A), LCA(B))$
|
||||
5. 两点的LCA一定在两点的树上最短路径上。
|
||||
|
||||
### LCA算法流程
|
||||
|
||||
#### 朴素LCA
|
||||
|
||||
每次找到深度较大的点,往上跳,显然最后这两个点会汇聚在同一个点,这个点就是LCA
|
||||
|
||||
朴素算法需要DFS整棵树,预处理和单次查询的最坏时间复杂度为 $O(n)$
|
||||
|
||||
参考代码就不写了--
|
||||
|
||||
#### 倍增LCA
|
||||
|
||||
记 $fa_{x, i}$ 表示点 $x$ 的第 $2 ^ i$ 个祖先,显然 $fa_{x, i} = fa_{fa_{x, i - 1}, i - 1}$,可以以 $O(n log n)$ 的复杂度预处理。
|
||||
|
||||
查询LCA时,我们求出两点深度之差 $\Delta dep$,通过对其的二进制拆分,将两个点跳到同一高度。深度相同后,若 $u, v$ 仍不相等,则两点同时向上跳 $2 ^ i$ 步,一直尝试到两节点不相等,重复这个步骤直到两节点相等且儿子不相等,时间复杂度为 $O(log n)$
|
||||
|
||||
##### 倍增LCA参考代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
using namespace std;
|
||||
const int maxn = 200005;
|
||||
int fa[20][maxn], dep[maxn];
|
||||
void get_fa(int n)
|
||||
{
|
||||
for (int k = 1; k < 20; ++k)
|
||||
{
|
||||
for (int u = 1; u <= n; ++u)
|
||||
{
|
||||
fa[k][u] = fa[k - 1][fa[k - 1][u]];
|
||||
}
|
||||
}
|
||||
}
|
||||
int LCA(int u, int v)
|
||||
{
|
||||
if (dep[u] > dep[v])
|
||||
swap(u, v);
|
||||
for (int k = 19; k >= 0; --k)
|
||||
if (dep[fa[k][v]] >= dep[u])
|
||||
v = fa[k][v];
|
||||
if (u == v)
|
||||
return u;
|
||||
for (int k = 19; k >= 0; --k)
|
||||
{
|
||||
if (fa[k][u] != fa[k][v])
|
||||
u = fa[k][u], v = fa[k][v];
|
||||
}
|
||||
return fa[0][u];
|
||||
}
|
||||
```
|
||||
|
||||
## 树链剖分
|
||||
|
||||
### 树链剖分概念
|
||||
|
||||
树链剖分用于把一棵树分割成若干条链的形式,以维护树上路径信息。
|
||||
|
||||
树链剖分有多种形式,如**重链剖分**,**长链剖分**和用于Link/Cut Tree的剖分(有时又称实链剖分)。在没有特殊说明的情况下,树链剖分都指的是***重链剖分***
|
||||
|
||||
重链剖分可以将树上任意路径划分成 $\le O(log n)$ 条链,每条链上点的深度互不相同(即自底向上的一条链,所有点的LCA为链的一个端点)
|
||||
|
||||
### 树链剖分基本定义
|
||||
|
||||
- **重子节点**表示其子节点中对打的子节点。如果有多个最大的,则任取其一。
|
||||
- **轻子节点**表示除去重子节点外其他所有子节点。
|
||||
- 从这个节点到重子节点的边叫做**重边**
|
||||
- 到其余子节点的边叫做**轻边**
|
||||
- 若干条首尾相接的重边称作**重链**
|
||||
- 把最后不在重链中的节点也当作一个重链,那么整棵树便被分割为若干条重链。
|
||||
|
||||
整个过程可以用两次DFS完成,一边找重链,一边找落单节点
|
||||
|
||||
举例:
|
||||
|
||||

|
||||
|
||||
### 树链剖分约定定义
|
||||
|
||||
- $fa(x)$ 表示 $x$ 在树上的父亲
|
||||
- $dep(x)$ 表示 $x$ 在树上的深度
|
||||
- $siz(x)$ 表示 $x$ 子树节点数量
|
||||
- $son(x)$ 表示 $x$ 的重儿子
|
||||
- $top(x)$ 表示 $x$ 所在重链的顶部(深度最小)的节点
|
||||
- $dfn(x)$ 表示 $x$ 的DFS序,也是在线段树中的编号
|
||||
- $rnk(x)$ 表示 DFS序所对应的节点编号,显然 $rnk(dfn(x)) = x$
|
||||
|
||||
在进行两次dfs时,同时也处理这些值,第一次求出 $fa, dep, siz$,第二次求出 $top, dfn, rnk$
|
||||
|
||||
### 树链剖分基本性质
|
||||
|
||||
- 树上每个节点仅属于一条重链
|
||||
- 重链的开头一定是轻子节点
|
||||
- 在剖分的时候,因为优先遍历重边,所以树的DFS序上,重链内部的DFS序是里连续的。
|
||||
|
||||
### 树链剖分例题
|
||||
|
||||
#### 树链剖分例题0
|
||||
|
||||
给定一棵 $n$ 个点的带权树, $q$ 次询问 $u$ 到 $v$ 的最短距离。
|
||||
|
||||
##### 树链剖分例题0题解
|
||||
|
||||
公式: $distance(u, v) = dep(u) + dep(u) - 2dep(LCA(u, v))$
|
||||
|
||||
#### P1967 货车运输
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P1967)
|
||||
|
||||
A国有 $n$ 座城市,编号从 $1$ 到 $n$,城市之间有 $m$ 条双向道路,每一条路都有限重。
|
||||
|
||||
现有 $q$ 辆货车运送货物,第 $i$ 辆货车起点为 $s_i$,终点为 $t_i$。司机们想知道在不超过限重的情况下,可以运多重的货物。
|
||||
|
||||
$n, m, q \le 10^5$
|
||||
|
||||
##### P1967题解
|
||||
|
||||
求出最大生成树,问题就转换为了树上链的最小值。
|
||||
|
||||
主要有以下两种解法:
|
||||
|
||||
- 倍增。预处理 $w(u, k)$ 表示从 $u$ 往上跳 $k$ 步,经过的边权最小值。在算LCA时顺便计算最小值即可。
|
||||
- 树剖+ST表。树剖后路径变成 $O(log n)$ 条重链,也就是 $O(log n)$ 次询问区间min,用ST表求即可。
|
||||
|
||||
两种方法时间复杂度均为 $O(n log n)$
|
||||
|
||||
##### P1967示例代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
const int MAXN = 100010;
|
||||
using namespace std;
|
||||
bool f;
|
||||
int n, m, q;
|
||||
struct node
|
||||
{
|
||||
int st, ed, v;
|
||||
} Edge[MAXN];
|
||||
int pre[MAXN];
|
||||
bool cmp(node A, node B) { return A.v > B.v; }
|
||||
int fFind(int x)
|
||||
{
|
||||
if (pre[x] == x)
|
||||
return x;
|
||||
return pre[x] = fFind(pre[x]);
|
||||
}
|
||||
struct nd
|
||||
{
|
||||
vector<node> G[MAXN];
|
||||
int F[MAXN][20], num[MAXN][20], lg[MAXN], deep[MAXN], root[MAXN];
|
||||
bool mark[MAXN], vis[MAXN];
|
||||
void dfs(int x, int fa, int val, int rt)
|
||||
{
|
||||
root[x] = rt;
|
||||
F[x][0] = fa;
|
||||
num[x][0] = val;
|
||||
deep[x] = deep[fa] + 1;
|
||||
for (int i = 1; (1 << i) <= deep[x]; i++)
|
||||
F[x][i] = F[F[x][i - 1]][i - 1], num[x][i] = min(num[x][i - 1], num[F[x][i - 1]][i - 1]);
|
||||
for (int i = 0; i < G[x].size(); i++)
|
||||
{
|
||||
int t = G[x][i].ed, val = G[x][i].v;
|
||||
if (t == fa)
|
||||
continue;
|
||||
dfs(t, x, val, rt);
|
||||
}
|
||||
}
|
||||
int LCA(int x, int y)
|
||||
{
|
||||
if (deep[x] < deep[y])
|
||||
swap(x, y);
|
||||
int res = 2e9 + 7;
|
||||
while (deep[x] > deep[y])
|
||||
res = min(res, num[x][lg[deep[x] - deep[y]] - 1]), x = F[x][lg[deep[x] - deep[y]] - 1];
|
||||
if (x == y)
|
||||
return res;
|
||||
for (int i = lg[deep[x]] - 1; i >= 0; i--)
|
||||
{
|
||||
if (F[x][i] == F[y][i])
|
||||
continue;
|
||||
res = min(res, num[x][i]);
|
||||
res = min(res, num[y][i]);
|
||||
x = F[x][i], y = F[y][i];
|
||||
}
|
||||
res = min(res, num[x][0]);
|
||||
res = min(res, num[y][0]);
|
||||
return res;
|
||||
}
|
||||
void work()
|
||||
{
|
||||
for (int i = 1; i <= MAXN - 10; i++)
|
||||
lg[i] = lg[i - 1] + ((1 << lg[i - 1]) == i);
|
||||
for (int i = 1; i <= n; i++)
|
||||
pre[i] = i;
|
||||
for (int i = 1; i <= m; i++)
|
||||
{
|
||||
int x, y, z;
|
||||
scanf("%d %d %d", &x, &y, &z);
|
||||
Edge[i] = node{x, y, z};
|
||||
}
|
||||
sort(Edge + 1, Edge + 1 + m, cmp);
|
||||
for (int i = 1; i <= m; i++)
|
||||
{
|
||||
int fx = fFind(Edge[i].st), fy = fFind(Edge[i].ed);
|
||||
if (fx == fy)
|
||||
continue;
|
||||
mark[Edge[i].st] = mark[Edge[i].ed] = true;
|
||||
pre[fx] = fy;
|
||||
G[Edge[i].st].push_back(node{0, Edge[i].ed, Edge[i].v});
|
||||
G[Edge[i].ed].push_back(node{0, Edge[i].st, Edge[i].v});
|
||||
}
|
||||
for (int i = 1; i <= n; i++)
|
||||
if (!root[i])
|
||||
dfs(i, 0, 0, i);
|
||||
scanf("%d", &q);
|
||||
for (int i = 1; i <= q; i++)
|
||||
{
|
||||
int x, y;
|
||||
scanf("%d %d", &x, &y);
|
||||
if (root[x] != root[y])
|
||||
{
|
||||
puts("-1");
|
||||
continue;
|
||||
}
|
||||
printf("%d\n", LCA(x, y));
|
||||
}
|
||||
}
|
||||
} r;
|
||||
bool fflag;
|
||||
int main()
|
||||
{
|
||||
scanf("%d %d", &n, &m);
|
||||
r.work();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最短路
|
||||
|
||||
### 最短路算法
|
||||
|
||||
比较:
|
||||
|
||||
|算法|Floyd|Bellman–Ford|Dijkstra|Johnson|
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
|最短路类型|多源|单源|单源|多源|
|
||||
|作用于|任意 |任意|无负边|任意|
|
||||
|能否检测负环?|Y|Y|N|Y|
|
||||
|时间复杂度| $O(N^3)$ | $O(NM)$ | $O(M \log M)$ | $O(NM \log M)$|
|
||||
|
||||
#### Floyd
|
||||
|
||||
三个 for 进行松弛操作,核心代码: `dis[x][y] = min(dis[x][y], dis[x][k] + dis[k][y])`
|
||||
|
||||
支持负边权。
|
||||
|
||||
#### Bellman–Ford
|
||||
|
||||
支持负边权,判断路径负环。
|
||||
|
||||
#### SPFA
|
||||
|
||||
死了。
|
||||
|
||||
队列优化的 Bellman–Ford,最坏时间复杂度与 Bellman–Ford 相同。
|
||||
|
||||
同样支持负边权,判断路径负环
|
||||
|
||||
#### Dijkstra
|
||||
|
||||
不支持负边权。
|
||||
|
||||
### 最短路例题
|
||||
|
||||
---
|
||||
|
||||
## 最小生成树
|
||||
|
||||
### 最小生成树定义
|
||||
|
||||
定义无向连通图的**最小生成树**(Minimum Spanning Tree,MST)为边权**和**最小的生成树。
|
||||
|
||||
注意:只有连通图才有生成树,而对于非连通图,只存在生成森林。任意生成树本身也是生成森林
|
||||
|
||||
### 最小生成树算法
|
||||
|
||||
#### Kruskal 算法
|
||||
|
||||
Kruskal 算法是一种常见并且好写的最小生成树算法,由 Kruskal 发明。该算法的基本思想是从小到大加入**边**。
|
||||
|
||||
复杂度 $O(m \log m)$,主要是排序( $m$代表边的数量)
|
||||
|
||||
##### Kruskal部分代码
|
||||
|
||||
```cpp
|
||||
vector<node> res;
|
||||
sort(e.begin(), e.end());
|
||||
for (auto i : e)
|
||||
{
|
||||
if (fa(u) != fa(v))
|
||||
{
|
||||
merge(u, v);
|
||||
res.push_back(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Prim 算法
|
||||
|
||||
不同于Kruskal,Prim的策略类似 Dijkstra,每次寻找已经选择的点的距离最近的未选择的**点**,直到所有点都连通结束。
|
||||
|
||||
复杂度 $O(n + m) \log n$
|
||||
|
||||
##### Prim代码(来自OI Wiki)
|
||||
|
||||
```cpp
|
||||
// 使用二叉堆优化的 Prim 算法。
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <queue>
|
||||
using namespace std;
|
||||
const int N = 5050, M = 2e5 + 10;
|
||||
|
||||
struct E {
|
||||
int v, w, x;
|
||||
} e[M * 2];
|
||||
|
||||
int n, m, h[N], cnte;
|
||||
|
||||
void adde(int u, int v, int w) { e[++cnte] = E{v, w, h[u]}, h[u] = cnte; }
|
||||
|
||||
struct S {
|
||||
int u, d;
|
||||
};
|
||||
|
||||
bool operator<(const S &x, const S &y) { return x.d > y.d; }
|
||||
|
||||
priority_queue<S> q;
|
||||
int dis[N];
|
||||
bool vis[N];
|
||||
|
||||
int res = 0, cnt = 0;
|
||||
|
||||
void Prim() {
|
||||
memset(dis, 0x3f, sizeof(dis));
|
||||
dis[1] = 0;
|
||||
q.push({1, 0});
|
||||
while (!q.empty()) {
|
||||
if (cnt >= n) break;
|
||||
int u = q.top().u, d = q.top().d;
|
||||
q.pop();
|
||||
if (vis[u]) continue;
|
||||
vis[u] = 1;
|
||||
++cnt;
|
||||
res += d;
|
||||
for (int i = h[u]; i; i = e[i].x) {
|
||||
int v = e[i].v, w = e[i].w;
|
||||
if (w < dis[v]) {
|
||||
dis[v] = w, q.push({v, w});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
cin >> n >> m;
|
||||
for (int i = 1, u, v, w; i <= m; ++i) {
|
||||
cin >> u >> v >> w, adde(u, v, w), adde(v, u, w);
|
||||
}
|
||||
Prim();
|
||||
if (cnt == n)
|
||||
cout << res;
|
||||
else
|
||||
cout << "No MST.";
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### Boruvka 算法
|
||||
|
||||
这个算法是前面两种算法的结合,可以用于求解非连通图的最小生成森林
|
||||
|
||||
初始时,每一个边均独属于一个连通块, $E' \leftarrow \varnothing$,算法步骤如下:
|
||||
|
||||
1. 计算每个点分别属于哪个连通块。将每个连通块都设为「没有最小边」。
|
||||
2. 遍历每条边 (u, v),如果 u 和 v 不在同一个连通块,就用这条边的边权分别更新 u 和 v 所在连通块的最小边。
|
||||
3. 如果所有连通块都没有最小边,退出程序,此时的 E' 就是原图最小生成森林的边集。否则,将每个有最小边的连通块的最小边加入 E',返回第一步。
|
||||
|
||||
复杂度 $O(m \log n)$
|
||||
|
||||
##### Boruvka伪代码
|
||||
|
||||

|
||||
|
||||
### 延伸:次小生成树
|
||||
|
||||
#### 非严格次小生成树
|
||||
|
||||
在无向图中,边权和最小的满足边权和**大于等于**最小生成树边权和的生成树
|
||||
|
||||
##### 非严格次小生成树求法
|
||||
|
||||
- 求出无向图的最小生成树 $T$,设其权值和为 $M$
|
||||
- 遍历每条未被选中的边 $e = (u, v, w)$,找到 $T$ 中 $u$ 到 $v$ 路径上边权最大的一条边 $e' = (s, t, w')$,则在 $T$ 中以 $e$ 替换 $e'$,可得一棵权值和为 $M' = M + w - w'$ 的生成树 $T'$.
|
||||
- 对所有替换得到的答案 $M'$ 取最小值即可
|
||||
|
||||
如何求 $u, v$ 路径上的边权最大值呢?
|
||||
|
||||
我们可以使用倍增来维护,预处理出每个节点的 $2^i$ 级祖先及到达其 $2^i$ 级祖先路径上最大的边权,这样在倍增求 LCA 的过程中可以直接求得。
|
||||
|
||||
#### 严格次小生成树
|
||||
|
||||
在无向图中,边权和最小的满足边权和**严格大于**最小生成树边权和的生成树
|
||||
|
||||
##### 严格次小生成树求法
|
||||
|
||||
考虑刚才的非严格次小生成树求解过程,为什么求得的解是非严格的?
|
||||
|
||||
因为最小生成树保证生成树中 $u$ 到 $v$ 路径上的边权最大值一定**不大于**其他从 $u$ 到 $v$ 路径的边权最大值。换言之,当我们用于替换的边的权值与原生成树中被替换边的权值相等时,得到的次小生成树是非严格的。
|
||||
|
||||
解决的办法很自然:我们维护到 $2^i$ 级祖先路径上的最大边权的同时维护**严格次大边权**,当用于替换的边的权值与原生成树中路径最大边权相等时,我们用严格次大值来替换即可。
|
||||
|
||||
### 延伸:瓶颈生成树
|
||||
|
||||
对于一个无向图 $G$ 的所有生成树,瓶颈生成树的最大的边权值在 $G$ 的所有生成树中最小。
|
||||
|
||||
#### 瓶颈生成树性质
|
||||
|
||||
最小生成树是瓶颈生成树的**充分不必要条件**。 即最小生成树一定是瓶颈生成树,而瓶颈生成树不一定是最小生成树。
|
||||
|
||||
证明:可以运用反证法证明:我们设最小生成树中的最大边权为 $w$,如果最小生成树不是瓶颈生成树的话,则瓶颈生成树的所有边权都小于 $w$,我们只需删去原最小生成树中的最长边,用瓶颈生成树中的一条边来连接删去边后形成的两棵树,得到的新生成树一定比原最小生成树的权值和还要小,这样就产生了矛盾。
|
||||
|
||||
### 延伸:Kruskal 重构树
|
||||
|
||||
在跑 Kruskal 的过程中我们会从小到大加入若干条边。现在我们仍然按照这个顺序。
|
||||
|
||||
首先新建 $n$ 个集合,每个集合恰有一个节点,点权为 $0$。
|
||||
|
||||
每一次加边会合并两个集合,我们可以新建一个点,点权为加入边的边权,同时将两个集合的根节点分别设为新建点的左儿子和右儿子。然后我们将两个集合和新建点合并成一个集合。将新建点设为根。
|
||||
|
||||
不难发现,在进行 $n - 1$ 轮之后我们得到了一棵恰有 $n$ 个叶子节点的二叉树,同时每个非叶子节点恰好有两个儿子。这棵树就叫 Kruskal 重构树。
|
||||
|
||||
举例:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### Kruskal重构树性质
|
||||
|
||||
1. 每个叶子代表一个节点,非叶子代表边
|
||||
2. 两个叶子的 LCA 的权值就是这两个节点的瓶颈路上的最大边
|
||||
|
||||
### 最小生成树例题
|
||||
|
||||
#### CF609E Minimum spanning tree for each edge
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF609E)
|
||||
|
||||
给定一个带权且连通的无向图 $G$
|
||||
|
||||
对于 $G$ 中的每条边 $e$,求包含 $e$ 的所有生成树中权值和最小的那个,输出权值和。
|
||||
|
||||
$N, M \le 2 \times 10^5$
|
||||
|
||||
##### CF609E题解
|
||||
|
||||
首先,很容易想出的,先求任意一个MST $T$,对于已经在 $e \in T$,答案直接输出。
|
||||
|
||||
如果 $e \notin T$,考虑将 $T$ 中 $u$ 到 $v$ 的路径中最长的边(记作 $e'$)断开,然后连接 $u, v$,则输出 $T \cup {e} - {e'}$ 的权值和即可。
|
||||
|
||||
证明:
|
||||
|
||||
考虑一个正确但是太慢的做法:
|
||||
|
||||
对于每一条不在 $T$ 中的 $e$,考虑将 $u, v$ 在并查集中合并,然后再去求新的MST,设这样的最小生成树为 $T'$,可以发现 $u, v$ 之间的路径的最大的边显然 $\notin T'$,所以就相当于是将最大的边替换为 $e$
|
||||
|
||||
##### CF609E代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
#define int long long
|
||||
using namespace std;
|
||||
int depth[200010], f[200020][21], vis[200010], fa[200010], cnt, head[200010];
|
||||
int n, m, x, y, q, start, end;
|
||||
int w[200020][21], T;
|
||||
struct node1
|
||||
{
|
||||
int dis, u, v, id;
|
||||
#define u(x) t[i].u
|
||||
#define v(x) t[i].v
|
||||
#define dis(x) t[i].dis
|
||||
} t[400010];
|
||||
struct node2
|
||||
{
|
||||
int dis, ver, nxt;
|
||||
} p[400010];
|
||||
int find(int x) { return (fa[x] == x) ? x : fa[x] = find(fa[x]); }
|
||||
void make_tree(int u)
|
||||
{
|
||||
vis[u] = 1;
|
||||
for (int j = 1; j <= 20; j++)
|
||||
|
||||
f[u][j] = f[f[u][j - 1]][j - 1], w[u][j] = max(w[u][j - 1], w[f[u][j - 1]][j - 1]);
|
||||
for (int i = head[u]; i; i = p[i].nxt)
|
||||
{
|
||||
int v = p[i].ver;
|
||||
if (vis[v])
|
||||
continue;
|
||||
depth[v] = depth[u] + 1, f[v][0] = u, w[v][0] = p[i].dis;
|
||||
make_tree(v);
|
||||
}
|
||||
}
|
||||
int lca(int x, int y)
|
||||
{
|
||||
int ans = 0;
|
||||
if (depth[x] < depth[y])
|
||||
swap(x, y);
|
||||
for (int i = 20; i >= 0; i--)
|
||||
if (depth[f[x][i]] >= depth[y])
|
||||
ans = max(ans, w[x][i]), x = f[x][i];
|
||||
if (x == y)
|
||||
return ans;
|
||||
for (int i = 20; i >= 0; i--)
|
||||
if (f[x][i] != f[y][i])
|
||||
ans = max(ans, max(w[x][i], w[y][i])), x = f[x][i], y = f[y][i];
|
||||
return max(ans, max(w[x][0], w[y][0]));
|
||||
}
|
||||
signed main()
|
||||
{
|
||||
#ifndef ONLINE_JUDGE
|
||||
freopen("1.in", "r", stdin);
|
||||
#endif
|
||||
auto add_edge = [](int u, int v, int l)
|
||||
{ p[++cnt].dis = l, p[cnt].nxt = head[u], p[cnt].ver = v, head[u] = cnt; };
|
||||
scanf("%lld%lld", &n, &m);
|
||||
for (int i = 1; i <= m; i++)
|
||||
{
|
||||
scanf("%lld%lld%lld", &u(i), &v(i), &dis(i));
|
||||
t[i].id = i;
|
||||
}
|
||||
sort(t + 1, t + m + 1, [](node1 a, node1 b)
|
||||
{ return a.dis < b.dis; });
|
||||
for (int i = 1; i <= n; i++)
|
||||
fa[i] = i;
|
||||
for (int i = 1; i <= m; i++)
|
||||
{
|
||||
int x = u(i), y = t[i].v, fx = find(x), fy = find(y);
|
||||
if (fx != fy)
|
||||
{
|
||||
fa[fx] = fy;
|
||||
add_edge(fx, fy, dis(i)), add_edge(fy, fx, dis(i));
|
||||
T += dis(i);
|
||||
}
|
||||
}
|
||||
make_tree(1);
|
||||
int ans = 0;
|
||||
sort(t + 1, t + m + 1, [](node1 a, node1 b)
|
||||
{ return a.id < b.id; });
|
||||
for (int i = 1; i <= m; i++)
|
||||
{
|
||||
ans = T + dis(i) - lca(u(i), v(i));
|
||||
printf("%lld\n", ans);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### AT_abc282_e Choose Two and Eat One
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/AT_abc282_e)
|
||||
|
||||
给定 $N$ 和 $M$,有 $N$ 个球,第 $i$ 个球上的数字是 $a_i$。进行以下操作 $n-1$ 次:
|
||||
|
||||
- 选中两个不同的球,获得 $(a_i^{a_j} + a_j^{a_i}) \bmod m$ 的分数,并且选中其中一个球扔掉
|
||||
|
||||
求最后的最大分数和
|
||||
|
||||
$N \le 500, 1 \le a_i \lt m, 2 \le M \le 10^9$
|
||||
|
||||
##### AT_abc282_e题解
|
||||
|
||||
发现每次操作均不影响 $a_i, a_j$ 的权值。
|
||||
|
||||
考虑将所有操作 $(i, j)$ 建图,图中不会出现环。
|
||||
|
||||
反过来说,每一棵生成树都能对应一个操作序列(每次消去一个叶子节点)
|
||||
|
||||
所以,建一个带权图, $i, j$ 之间边权为 $(a_i^{a_j} + a_j^{a_i}) \bmod m$,答案即为最大生成树的权值和。
|
||||
|
||||
##### AT_abc282_e代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
#define int long long
|
||||
using namespace std;
|
||||
int n, mod;
|
||||
int a[505];
|
||||
int fa[5000005];
|
||||
int cnt = 0;
|
||||
struct node
|
||||
{
|
||||
int u, v, w;
|
||||
const bool operator<(const node &x) const
|
||||
{
|
||||
return w > x.w;
|
||||
}
|
||||
};
|
||||
int fat(int x)
|
||||
{
|
||||
return fa[x] == x ? x : fa[x] = fat(fa[x]);
|
||||
}
|
||||
void merge(int x, int y)
|
||||
{
|
||||
fa[fat(x)] = fat(y);
|
||||
}
|
||||
int power(int a, int b)
|
||||
{
|
||||
int sum = 1;
|
||||
while (b)
|
||||
{
|
||||
if (b & 1)
|
||||
sum = sum * a % mod;
|
||||
a = a * a % mod;
|
||||
b >>= 1;
|
||||
}
|
||||
return sum % mod;
|
||||
}
|
||||
int pows(int i, int j)
|
||||
{
|
||||
return (power(a[i], a[j]) + power(a[j], a[i])) % mod;
|
||||
}
|
||||
vector<node> edge;
|
||||
signed main()
|
||||
{
|
||||
scanf("%lld%lld", &n, &mod);
|
||||
for (int i = 1; i <= n; ++i)
|
||||
scanf("%lld", &a[i]);
|
||||
for (int i = 1; i <= n; ++i)
|
||||
{
|
||||
for (int j = 1; j < i; ++j)
|
||||
{
|
||||
edge.push_back((node){
|
||||
i,
|
||||
j,
|
||||
pows(i, j)});
|
||||
fa[cnt] = cnt;
|
||||
++cnt;
|
||||
}
|
||||
}
|
||||
int T = 0;
|
||||
sort(edge.begin(), edge.end());
|
||||
for (node i : edge)
|
||||
{
|
||||
if (fat(i.u) != fat(i.v))
|
||||
{
|
||||
T += i.w;
|
||||
merge(i.u, i.v);
|
||||
}
|
||||
}
|
||||
printf("%lld\n", T);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### CF1857G Counting Graphs
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF1857G)
|
||||
|
||||
给定一棵有边权的树 $T$ 和一个正整数 $S$
|
||||
|
||||
求有多少个 $(\bmod \ 998244353)$ 简单图 $G$ 满足:
|
||||
- $G$ 中的所有边的边权不超过 $S$
|
||||
- $G$ 的 MST 唯一,且恰好为 $T$
|
||||
|
||||
$2 \le N \le 2 \times 10^5, S \le 10^9$
|
||||
|
||||
##### CF1857G题解
|
||||
|
||||
一个非常显然的事情是:总方案数即为每条边方案数之积。
|
||||
|
||||
树边已经确定,考察每条非树边 $(u, v)$ 可以怎么取。给定的树 $T$ 是唯一最小生成树,这意味着非树边 $(u, v)$ 要么不存在,要么权值大于 $T$ 上 $(u, v)$ 之间任意一条边的权值。设 T 上 $(u, v)$ 间的最大边权为 $k$,则 $(u, v)$ 对答案的贡献为 $S − k + 1$。
|
||||
|
||||
但我们无法枚举每条非树边计算贡献,因为复杂度为 $O(n^2)$。考虑将“一类”非树边放到一起计算。
|
||||
|
||||
考虑 Kruskal 算法的过程,每次取出权值最小的边 $(u, v, w)$ 加入最小生成树,并将两个连通块 $B_1, B_2$ 合并。当一条边 $(u, v, w)$ 加入最小生成树时,它就是跨越两个连通块 $B_1, B_2$ 的任意一对点间的最大权值。这就意味着对于每一对 $B_1 \times B_2$ 中的点对($\times$ 是集合直积,$(u, v)$ 除外),这条边要么不存在,要么权值大于 $w$。
|
||||
|
||||
我们用桶统计出对于每个 $w$,有多少条边的要求是“要么不存在,要么权值大于 $w$”,并用快速幂计算即可。
|
||||
|
||||
##### CF1857G代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
#define int long long
|
||||
using namespace std;
|
||||
const int N = 2e5 + 5, mod = 998244353;
|
||||
int T, n, S;
|
||||
struct Edge
|
||||
{
|
||||
int u, v, w;
|
||||
} e[N];
|
||||
|
||||
int power(int x, int y)
|
||||
{
|
||||
int ans = 1;
|
||||
while (y)
|
||||
{
|
||||
if (y & 1)
|
||||
ans = ans * x % mod;
|
||||
y >>= 1, x = x * x % mod;
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
struct node
|
||||
{
|
||||
int fa[N], sz[N];
|
||||
int find(int x)
|
||||
{
|
||||
return x == fa[x] ? x : fa[x] = find(fa[x]);
|
||||
}
|
||||
void merge(int x, int y)
|
||||
{
|
||||
if (find(x) == find(y))
|
||||
return;
|
||||
x = find(x), y = find(y);
|
||||
fa[x] = y, sz[y] += sz[x];
|
||||
return;
|
||||
}
|
||||
} cxy;
|
||||
map<int, int> cnt;
|
||||
|
||||
signed main()
|
||||
{
|
||||
#ifndef ONLINE_JUDGE
|
||||
freopen("2.in", "r", stdin);
|
||||
#endif
|
||||
scanf("%lld", &T);
|
||||
while (T--)
|
||||
{
|
||||
cnt.clear();
|
||||
scanf("%lld%lld", &n, &S);
|
||||
for (int i = 1; i < n; ++i)
|
||||
scanf("%lld%lld%lld", &e[i].u, &e[i].v, &e[i].w);
|
||||
sort(e + 1, e + n, [](Edge a, Edge b)
|
||||
{ return a.w < b.w; });
|
||||
for (int i = 1; i <= n; ++i)
|
||||
cxy.fa[i] = i, cxy.sz[i] = 1;
|
||||
for (int i = 1; i < n; ++i)
|
||||
{
|
||||
int u = e[i].u, v = e[i].v, w = e[i].w;
|
||||
cnt[w] += cxy.sz[cxy.find(u)] * cxy.sz[cxy.find(v)] - 1;
|
||||
cxy.merge(u, v);
|
||||
}
|
||||
int ans = 1;
|
||||
for (auto i : cnt)
|
||||
{
|
||||
if (i.second)
|
||||
ans = ans * power(S - i.first + 1, i.second) % mod;
|
||||
}
|
||||
printf("%lld\n", ans);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### P4768 归程
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P4768)
|
||||
|
||||
给定一个无向图,每条边有长度以及一个海拔
|
||||
|
||||
有 $Q$ 次询问,每次给定起点 $v$ 以及一个限制 $p$,从 $v$ 出发,先开车到海拔 $\gt p$ 的边到某一个节点,然后步行到 $1$ 号节点,求最短步行路程。询问强制在线。
|
||||
|
||||
$N \le 2 \times 10^5, M, Q \le 4 \times 10^5$
|
||||
|
||||
##### P4768题解
|
||||
|
||||
考虑 Kruskal 重构树,预处理每个子树内到 $1$ 号节点的最短距离。
|
||||
|
||||
##### P4768代码
|
||||
|
||||
#### CF1706E Qpwoeirut and Vertices
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF1706E)
|
||||
|
||||
#### CF888G Xor-MST
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF888G)
|
||||
|
||||
#### CF1120D Power Tree
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF1120D)
|
||||
|
||||
给定一棵有根树, $1$ 为根。定义 $u$ 为叶子当且仅当它**不是根且度数为 $1$**
|
||||
|
||||
你可以选择花费 $w_u$ 的代价控制 $u$ 点。当一个点被控制时你可以选择给它的子树内的叶子的点权都加上一个值 $v$。你需要控制若干个点,使得花费的总代价尽量的少,且无论怎样规定所有叶子的初始点权,都可以通过调整你选择的点的值来让所有叶子的点权变为 $0$。
|
||||
|
||||
$2 \le N \le 2 \times 10^5$
|
||||
|
||||
##### CF1120D题解
|
||||
|
||||
一个子树的 DFS 序是连续的,所以考虑对叶子节点 DFS 标号,这样子树内叶子节点的编号也连续。
|
||||
|
||||
对于一个点的操作,可以视为对一个区间 $[l, r]$ 加上 $v$。进一步的,
|
||||
|
||||
---
|
||||
|
||||
## 强联通分量
|
||||
|
||||
### 强联通分量概念
|
||||
|
||||
首先需要明确,强联通的概念只存在于有向图中,无向图只有联通和不联通。
|
||||
|
||||
- **弱联通**:对于有向图 $G = (V, E)$,如果将其视为无向图,它是联通的,则 $G$ 是一个弱联通图。
|
||||
- **强联通**:对于有向图 $G = (V, E)$,如果对于每两个节点 $x, y \in V$,都存在路径 $d_0 = x, d_k = y$,则 $G$ 为强联通图(即每两个节点都可以相互到达)。
|
||||
|
||||

|
||||
|
||||
在本图中, $0$ 和 $2$ 是弱联通的,也是强联通的,因为存在 $0$ 到 $2$ 的路径和 $2$ 到 $0$ 的路径。而 $1$ 和 $3$ 则是弱联通的,因为将其视为无向图时 $1$ 和 $3$ 是联通的,而视为有向图时则只存在 $1$ 到 $3$ 的路径。
|
||||
|
||||
- 强联通子图:对于有向图 $G = (V, E)$,如果存在 $V$ 的子集 $V_0$ 满足 $\forall x, y \in V_0$, $x$ 与 $y$ 都是强联通的,则 $V_0$ 是一个强联通子图。
|
||||
- 强联通分量(SCC):极大的强联通子图(好粗略的概念。。。但确实是这样的)
|
||||
|
||||
### Tarjan
|
||||
|
||||
#### Tarjan 前置概念
|
||||
|
||||
对一个有向图 dfs 遍历后会生成一棵树。
|
||||
|
||||
- 在 dfs 生成树中的边称作树边,不在的称为非树边。
|
||||
- $dfn_x$ 表示 $x$ 的 dfs 序,即 $x$ 是第几个遍历到的。
|
||||
- $low_x$ 表示 $x$ 节点在 dfs 生成树中的子树节点以及从子树节点经过一条边到达的节点的最小 $dfn$ 值。
|
||||
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
settings.json
|
||||
tmp
|
||||
tmp.*
|
||||
latex.md
|
||||
*.exe
|
||||
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,793 @@
|
||||
# 数学
|
||||
|
||||
- [数学](#数学)
|
||||
- [费马小定理,逆元,同余](#费马小定理逆元同余)
|
||||
- [同余](#同余)
|
||||
- [同余的表示](#同余的表示)
|
||||
- [同余的性质](#同余的性质)
|
||||
- [逆元](#逆元)
|
||||
- [求解逆元](#求解逆元)
|
||||
- [线性求解逆元](#线性求解逆元)
|
||||
- [费马小定理,逆元,同余例题](#费马小定理逆元同余例题)
|
||||
- [B3717 组合数问题](#b3717-组合数问题)
|
||||
- [B3717题解](#b3717题解)
|
||||
- [P3403 跳楼机](#p3403-跳楼机)
|
||||
- [P3403题解](#p3403题解)
|
||||
- [P3403代码](#p3403代码)
|
||||
- [欧几里得算法和中国剩余定理](#欧几里得算法和中国剩余定理)
|
||||
- [欧几里得算法](#欧几里得算法)
|
||||
- [前置知识:裴蜀定理](#前置知识裴蜀定理)
|
||||
- [扩展欧几里得](#扩展欧几里得)
|
||||
- [扩展欧几里得参考代码](#扩展欧几里得参考代码)
|
||||
- [欧几里得算法例题](#欧几里得算法例题)
|
||||
- [P1516 青蛙的约会](#p1516-青蛙的约会)
|
||||
- [P1516题解](#p1516题解)
|
||||
- [CF1010C Border](#cf1010c-border)
|
||||
- [CF1010C题解](#cf1010c题解)
|
||||
- [CF1010C代码](#cf1010c代码)
|
||||
- [CF1427E Xum](#cf1427e-xum)
|
||||
- [CF1427E题解](#cf1427e题解)
|
||||
- [中国剩余定理(CRT)](#中国剩余定理crt)
|
||||
- [CRT引入](#crt引入)
|
||||
- [CRT概念](#crt概念)
|
||||
- [CRT过程](#crt过程)
|
||||
- [CRT代码](#crt代码)
|
||||
- [扩展(EXCRT):模数不互质](#扩展excrt模数不互质)
|
||||
- [EXCRT两个方程](#excrt两个方程)
|
||||
- [EXCRT多个方程](#excrt多个方程)
|
||||
- [组合数学与矩阵初步](#组合数学与矩阵初步)
|
||||
- [组合数学部分](#组合数学部分)
|
||||
- [组合数的定义](#组合数的定义)
|
||||
- [组合恒等式](#组合恒等式)
|
||||
- [二项式定理](#二项式定理)
|
||||
- [二项式反演](#二项式反演)
|
||||
- [矩阵部分](#矩阵部分)
|
||||
- [矩阵简介](#矩阵简介)
|
||||
- [矩阵加减法](#矩阵加减法)
|
||||
- [矩阵数乘](#矩阵数乘)
|
||||
- [矩阵转置](#矩阵转置)
|
||||
- [矩阵乘法](#矩阵乘法)
|
||||
- [行列式](#行列式)
|
||||
- [$n$ 阶行列式](#n-阶行列式)
|
||||
- [行列式的意义](#行列式的意义)
|
||||
- [组合数学与矩阵初步例题](#组合数学与矩阵初步例题)
|
||||
- [P1962 斐波那契数列](#p1962-斐波那契数列)
|
||||
- [P1962矩阵解法](#p1962矩阵解法)
|
||||
- [P1962非矩阵解法](#p1962非矩阵解法)
|
||||
|
||||
## 费马小定理,逆元,同余
|
||||
|
||||
### 同余
|
||||
|
||||
#### 同余的表示
|
||||
|
||||
若 $a \bmod m = b \bmod m$,则我们称 $a, b$ 同余,记作 $a \equiv b \pmod m$。
|
||||
|
||||
注意:c++中,一个负数 mod 另一个数结果还是负数。如 $-11 \% 3 = -2$。而在数学中, $-11 \% 3 = 1$
|
||||
|
||||
若 $a \equiv b \bmod m$,则 $m | (a - b), a - b = km (k \in \mathbb{Z})$,其中 $\mathbb{Z}$ 表示整数集合
|
||||
|
||||
#### 同余的性质
|
||||
|
||||
- 自反性: $a \equiv a \pmod m$
|
||||
- 对称性:若 $a \equiv b \pmod m$,则 $b \equiv a \pmod m$
|
||||
- 传递性:若 $a \equiv b \pmod m, b \equiv c \pmod m$,则 $a \equiv c \pmod m$
|
||||
|
||||
类似等式,在同余式的两边同时进行相同的线性运算也不影响结果。
|
||||
|
||||
如:
|
||||
|
||||
$$
|
||||
a \equiv b \pmod m, c \equiv d \pmod m
|
||||
$$
|
||||
|
||||
$$
|
||||
\Downarrow
|
||||
$$
|
||||
|
||||
$$
|
||||
a \pm c \equiv b \pm d \pmod m, a \times c \equiv b \times d \pmod m
|
||||
$$
|
||||
|
||||
### 逆元
|
||||
|
||||
当 $ab \equiv 1 \pmod m$,则称在模 $m$ 意义下 $a, b$ 互为逆元。
|
||||
|
||||
可以将 $b$ 记作 $a^{-1}$
|
||||
|
||||
#### 求解逆元
|
||||
|
||||
求解模 $m$ 意义下的逆元,相当于求解 $ax \equiv 1 \pmod m$ 中的 $x$。
|
||||
|
||||
可以发现,当 $a$ 和 $m$ 互质时,原方程才有解,**即 $a$ 和 $m$ 互质时, $a$ 存在逆元**。
|
||||
|
||||
由于使用扩展欧几里得算法求解过于复杂,下面将会介绍一种更为简单地方法。
|
||||
|
||||
若 $\gcd(a, p) = 1$,则 $a^{p - 1} \equiv 1 \pmod p, p \in \mathbb P$,其中 $\mathbb P$ 表示质数集合。
|
||||
|
||||
根据这一定理,我们可以发现 $a \times a^{p - 2} \equiv 1 \pmod p$
|
||||
|
||||
所以,在模 $p$ 的意义下, $a$ 与 $a^{p - 2}$ 互为逆元。
|
||||
|
||||
故可以用快速幂来求解逆元,时间复杂度为 $O(\log m)$
|
||||
|
||||
需要注意的是,这种方法只适用于模数为质数的情况。非质数的情况需要用扩展欧几里得算法求解。
|
||||
|
||||
#### 线性求解逆元
|
||||
|
||||
费马小定理单次求解逆元的复杂度为 $O(\log n)$,若要求解 $[1, n)$ 中所有数的逆元,总的时间复杂度为 $O(n \log n)$
|
||||
|
||||
为了应对这种情况,还有一种 $O(n)$ 的递推方法,用于快速求解逆元。
|
||||
|
||||
首先,不难发现,对于任意的模数, $1$ 与本身都互为逆元。
|
||||
|
||||
接下来考虑递推。
|
||||
|
||||
假设已知 $[1, x)$ 中所有数的逆元,要求 $x$ 的逆元。
|
||||
|
||||
首先将 $n$ 表示为 $kx + t$
|
||||
|
||||
其中 $k=\lfloor \frac{n}{x} \rfloor, t = n \bmod x$
|
||||
|
||||
所以 $kx + t \equiv 0 \pmod n$
|
||||
|
||||
在两边同乘 $x^{-1} \times t^{-1}$ 后,可得 $kt^{-1} + x^{-1} \equiv 0 \pmod n$
|
||||
|
||||
移项后得到 $x^{-1} \equiv -kt^{-1} \pmod n$,即得到递推式 $x^{-1} \equiv -\lfloor \frac{n}{x} \rfloor (n \bmod x)^{-1} \pmod n$
|
||||
|
||||
而因为 $1 \le (n \bmod x) \le x$,则一定可以从已有的值推出 $x^{-1}$
|
||||
|
||||
### 费马小定理,逆元,同余例题
|
||||
|
||||
#### B3717 组合数问题
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/B3717)
|
||||
|
||||
给定多组 $n, m$,求 $C^n_m \bmod 998244353$
|
||||
|
||||
##### B3717题解
|
||||
|
||||
首先展开:
|
||||
|
||||
$C^n_m=\frac{n!}{m! \times (n - m)!}$
|
||||
|
||||
因此为了快速求解,就需要求出所有数的阶乘及其逆元。
|
||||
|
||||
如果对于每个数的阶乘单独求逆元,肯定无法通过此题。
|
||||
|
||||
所以我们记 $fact_x = x!$ 表示 $x$ 的阶乘, $inv_x$ 表示 $x$ 的逆元。
|
||||
|
||||
首先预处理 $inv_n = fact_n^{p - 2}$ (快速幂)
|
||||
|
||||
然后可以递推求解: $inv_x = \frac{1}{x!} = (x + 1) \times inv_{x + 1}$
|
||||
|
||||
预处理复杂度为 $O(n)$,单次查询复杂度为 $O(1)$
|
||||
|
||||
#### P3403 跳楼机
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P3403)
|
||||
|
||||
有一个高度为 $h$ 的大楼,每次可以往上跳 $x, y, z$ 层或回到第一层,请问一共有多少个楼层可以到达。
|
||||
|
||||
$1 \le h \le 2^{63} - 1, 1 \le x, y, z \le 10^5$
|
||||
|
||||
##### P3403题解
|
||||
|
||||
令 $x \le y \le z$
|
||||
|
||||
首先根据 $\bmod\ x$ 的余数将楼层分成 $x$ 类。
|
||||
|
||||
容易发现,如果同类楼层中某个楼层可以到达,则高于它的同类部分也可以到达。
|
||||
|
||||
因此可以求出每一类楼层最低可以到达的楼层,就可以算出这一类有几层可以到达。考虑如何计算每一类楼层的最低可到达(使用 $y, z$ 来凑)
|
||||
|
||||
设 $f_i$ 表示 $\bmod x = i$ 的楼层的最低可到达。
|
||||
|
||||
转移即为:
|
||||
|
||||
$$
|
||||
f_{(a + y) \bmod x} = min(f_{(a + y) \bmod x}, f_a + y) \\
|
||||
f_{(a + z) \bmod x} = min(f_{(a + z) \bmod x}, f_a + z) \\
|
||||
$$
|
||||
|
||||
发现可以使用最短路来解决。
|
||||
|
||||
##### P3403代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
#define int long long
|
||||
using namespace std;
|
||||
int h, f[100005], x, y, z;
|
||||
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
|
||||
bool inn[100005];
|
||||
signed main()
|
||||
{
|
||||
scanf("%lld%lld%lld%lld", &h, &x, &y, &z);
|
||||
for (int i = 1; i <= 100002; ++i)
|
||||
f[i] = LONG_LONG_MAX;
|
||||
f[0] = 1;
|
||||
q.push({1, 0});
|
||||
while (!q.empty())
|
||||
{
|
||||
int now = q.top().second;
|
||||
q.pop();
|
||||
if (inn[now])
|
||||
continue;
|
||||
inn[now] = 1;
|
||||
int to = (now + y) % x;
|
||||
if (f[to] > f[now] + y)
|
||||
{
|
||||
f[to] = f[now] + y;
|
||||
q.push({f[to], to});
|
||||
}
|
||||
to = (now + z) % x;
|
||||
if (f[to] > f[now] + z)
|
||||
{
|
||||
f[to] = f[now] + z;
|
||||
q.push({f[to], to});
|
||||
}
|
||||
}
|
||||
int ans = 0;
|
||||
for (int i = 0; i < x; i++)
|
||||
{
|
||||
if (f[i] > h)
|
||||
continue;
|
||||
ans += (h - f[i]) / x + 1;
|
||||
}
|
||||
printf("%lld", ans);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 欧几里得算法和中国剩余定理
|
||||
|
||||
### 欧几里得算法
|
||||
|
||||
对于任意的正整数 $a \ge b$,若 $a \equiv 0 \pmod b$, $\gcd(a, b) = b$,否则令 $a = kb + a \bmod b$
|
||||
|
||||
设 $S_i$ 表示 $i$ 的因数集合,注意到 $S_x \cap S_b = S_a \cap S_b$,所以可以得到如下推论:
|
||||
|
||||
$$\gcd(a, b) = \gcd(b, a \bmod b)$$
|
||||
|
||||
因此通过
|
||||
|
||||
```cpp
|
||||
int gcd(int a, int b){ return !b ? a : gcd(b, a % b);}
|
||||
```
|
||||
|
||||
即可求出 gcd
|
||||
|
||||
### 前置知识:裴蜀定理
|
||||
|
||||
$a, b$ 为非零整数,则存在整数 $x, y$,使得 $ax + by = \gcd(a, b)$
|
||||
|
||||
### 扩展欧几里得
|
||||
|
||||
对于任意正整数 $a, b$,考虑一元二次方程 $ax + by = c$ 的整数解
|
||||
|
||||
对于任意整数 $x$,存在 $y = \frac{c - ax}{b}$ 为唯一解,所以只需要考虑这个式子是否为整数,即是否存在 $x$ 使得
|
||||
|
||||
$$ax \equiv c \pmod b$$
|
||||
|
||||
考虑 $c = \gcd(a, b)$ 时如何找到一组解。
|
||||
|
||||
显然当 $a = 0$ 或 $b = 0$ 时可以直接找到解。(不是废话,等会递归时要用)
|
||||
|
||||
假设已知 $(a \bmod b)x + by = \gcd(a, b)$ 的一组解 $x', y'$ 则:
|
||||
|
||||
$$
|
||||
(a \bmod b)x' + by' = \gcd(a, b)
|
||||
$$
|
||||
|
||||
$$
|
||||
ax + by = \gcd(a, b)
|
||||
$$
|
||||
|
||||
取 $x = x'$,两式相减得到:
|
||||
|
||||
$$
|
||||
(a - a \bmod b)x + b(y - y') = 0
|
||||
$$
|
||||
|
||||
注意到 $a - a \bmod b = b \times \lfloor \frac{a}{b} \rfloor$,因此该方程有整数解:
|
||||
|
||||
$$
|
||||
y = y' - \frac{(a - a \bmod b)x}{b} = y' - \lfloor \frac{a}{b} \rfloor x
|
||||
$$
|
||||
|
||||
然后递归求解即可(**看代码**)
|
||||
|
||||
易证 $|x| \le |b|, |y| \le |a|$
|
||||
|
||||
这样就求到了一组特解 $x_0, y_0$,考虑求令一组解 $x, y$
|
||||
|
||||
$$
|
||||
\left\{
|
||||
\begin{aligned}
|
||||
ax_0 &+ by_0 &= c \\
|
||||
ax &+ by &= c \\
|
||||
\end{aligned}
|
||||
\right.
|
||||
$$
|
||||
|
||||
$$
|
||||
\Downarrow
|
||||
$$
|
||||
|
||||
$$
|
||||
a(x - x_0) + b(y - y_0) = 0
|
||||
$$
|
||||
|
||||
$$
|
||||
\Downarrow
|
||||
$$
|
||||
|
||||
$$
|
||||
\left\{
|
||||
\begin{aligned}
|
||||
a(x &- x_0) &= k\ lcm(a, b) = k \frac{ab}{\gcd(a, b)} \\
|
||||
-b(y &- y_0) &= k\ lcm(a, b) = k \frac{ab}{\gcd(a, b)} \\
|
||||
\end{aligned}
|
||||
\right.
|
||||
$$
|
||||
|
||||
$$
|
||||
\Downarrow
|
||||
$$
|
||||
|
||||
$$
|
||||
\left\{
|
||||
\begin{aligned}
|
||||
x &= k \frac{b}{\gcd(a, b)} + x_0 \\
|
||||
y &= -k \frac{a}{\gcd(a, b)} + y_0 \\
|
||||
\end{aligned}
|
||||
\right.
|
||||
$$
|
||||
|
||||
这样就找到了令一组解
|
||||
|
||||
#### 扩展欧几里得参考代码
|
||||
|
||||
```cpp
|
||||
int exgcd(int a, int b, int &x, int &y)
|
||||
{
|
||||
if (b == 0)
|
||||
{
|
||||
x = 1, y = 0;
|
||||
return a;
|
||||
}
|
||||
int g = exgcd(b, a % b, y, x);
|
||||
y -= a / b * x;
|
||||
return g;
|
||||
}
|
||||
```
|
||||
|
||||
### 欧几里得算法例题
|
||||
|
||||
#### P1516 青蛙的约会
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P1516)
|
||||
|
||||
两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止。可是它们出发之前忘记了一件很重要的事情,既没有问清楚对方的特征,也没有约定见面的具体位置。不过青蛙们都是很乐观的,它们觉得只要一直朝着某个方向跳下去,总能碰到对方的。但是除非这两只青蛙在同一时间跳到同一点上,不然是永远都不可能碰面的。为了帮助这两只乐观的青蛙,你被要求写一个程序来判断这两只青蛙是否能够碰面,会在什么时候碰面。
|
||||
|
||||
我们把这两只青蛙分别叫做青蛙 A 和青蛙 B,并且规定纬度线上东经 $0$ 度处为原点,由东往西为正方向,单位长度 $1$ 米,这样我们就得到了一条首尾相接的数轴。设青蛙 A 的出发点坐标是 $x$,青蛙 B 的出发点坐标是 $y$。青蛙 A 一次能跳 $m$ 米,青蛙 B 一次能跳 $n$ 米,两只青蛙跳一次所花费的时间相同。纬度线总长 $L$ 米。现在要你求出它们跳了几次以后才会碰面。
|
||||
|
||||
$1 \le x \ne y \le 2 \times 10^{9}, 1 \le m, n \le 2 \times 10^{9}, 1 \le L \le \text{INTMAX}$。
|
||||
|
||||
##### P1516题解
|
||||
|
||||
设 $k$ 时刻相遇。
|
||||
|
||||
$$
|
||||
(m - n)k \equiv (y - x) \pmod l
|
||||
$$
|
||||
|
||||
若 $y - x$ 不是 $\gcd(m - n, l)$ 的倍数,则无解。
|
||||
|
||||
否则,令
|
||||
|
||||
$$
|
||||
g \leftarrow \gcd(y - x, l), b = \frac{y - x}{g}, a = \frac{m - n}{g}, l' = \frac{l}{g}
|
||||
$$
|
||||
|
||||
要求的值即为 $ak \equiv b \pmod {l'}$ 的 $k$
|
||||
|
||||
由于 $\gcd(l', a) = 1$, $a$ 在 $\bmod {l'}$ 意义下[逆元](#逆元)存在,即 $k \equiv ba^{-1} \pmod {l'}$
|
||||
|
||||
所以只需要找出满足
|
||||
|
||||
$$
|
||||
aa^{-1} - kl' = 1
|
||||
$$
|
||||
|
||||
的 $a^{-1}, k$ 即可,且 $0 \le a^{-1} \lt l$
|
||||
|
||||
复杂度 $O(\log V)$, $V$ 为输入数据范围最大值
|
||||
|
||||
#### CF1010C Border
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF1010C)
|
||||
|
||||
给定 $n$ 个数 $\{a_i\}_{i = 1}^{n}$,求这些数的线性组合模 $k$ 有多少种不同的值。
|
||||
|
||||
##### CF1010C题解
|
||||
|
||||
根据[裴蜀定理](#前置知识裴蜀定理):
|
||||
|
||||
$$
|
||||
\sum_{i = 1}^{n}c_i a_i = c
|
||||
$$
|
||||
|
||||
有解当且仅当 $c$ 是 $\gcd(a_1, \dots , a_n)$ 的倍数,也就是说原式的线性组合一定是 $\gcd(a_1, \dots , a_n)$ 的倍数。
|
||||
|
||||
这是二维[裴蜀定理](#前置知识裴蜀定理)( $ax + by = c$ 有解,当且仅当 $c$ 是 $\gcd(a, b)$ 的倍数)的扩展
|
||||
|
||||
考虑将方程变形:
|
||||
|
||||
$$
|
||||
\sum^{n}_{i = 1}c_i a_i \equiv c \pmod k \Rightarrow \sum^{n}_{i = 1} c_i a_i - c_{n + 1} a_{n + 1} = c
|
||||
$$
|
||||
|
||||
$$
|
||||
(0 \le c \le k, a_{n + 1} \leftarrow k)
|
||||
$$
|
||||
|
||||
也就是说有解的 $c$ 一定是 $\gcd(a_1, \dots , a_n, k)$ 的倍数。依次输出即可。
|
||||
|
||||
时间复杂度 $O((n + k) \log k)$
|
||||
|
||||
##### CF1010C代码
|
||||
|
||||
```cpp
|
||||
#include <bits/stdc++.h>
|
||||
#define int long long
|
||||
using namespace std;
|
||||
int a[100005];
|
||||
vector<int> q;
|
||||
signed main()
|
||||
{
|
||||
int n, k;
|
||||
int ans;
|
||||
scanf("%lld%lld", &n, &k);
|
||||
for (int i = 1; i <= n; ++i)
|
||||
{
|
||||
scanf("%lld", &a[i]);
|
||||
if (i == 1)
|
||||
ans = a[1];
|
||||
else
|
||||
ans = __gcd(ans, a[i]);
|
||||
}
|
||||
ans = __gcd(ans, k);
|
||||
int ns = 0;
|
||||
while (ans * ns < k)
|
||||
{
|
||||
q.push_back(ns * ans);
|
||||
++ns;
|
||||
}
|
||||
printf("%d\n", q.size());
|
||||
for (int i : q)
|
||||
printf("%lld ", i);
|
||||
puts("");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### CF1427E Xum
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/CF1427E)
|
||||
|
||||
给定一个集合 $\{x\}$, $x$ 是奇数,你可以对这个集合进行如下操作:
|
||||
|
||||
- 选取集合里的两个数 $x, y$,插入 $x + y$
|
||||
- 选取集合里的两个数 $x, y$,插入 $x \oplus y$
|
||||
|
||||
构造一个操作方式,使得序列内出现 $1$
|
||||
|
||||
##### CF1427E题解
|
||||
|
||||
[构造](../基础算法/基础算法.md#构造转换模拟)题。
|
||||
|
||||
考虑构造 $x, y$ 使得 $\gcd(x, y) = 1$ 即可使用[扩展欧几里得算法](#扩展欧几里得)构造出 $1$。
|
||||
|
||||
经过打表构造发现当 $x$ 是奇数, $y$ 可以取 $x \oplus 2^{k - 1}x$,其中 $k$ 为 $x$ 二进制下的长度,此时便可构造出 $\gcd(x, y) = 1$
|
||||
|
||||
### 中国剩余定理(CRT)
|
||||
|
||||
#### CRT引入
|
||||
|
||||
小学奥数问题:
|
||||
|
||||
> 有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?
|
||||
|
||||
转换为同余式如下:
|
||||
|
||||
$$
|
||||
\left\{
|
||||
\begin{aligned}
|
||||
x &\equiv 2 \pmod 3 \\
|
||||
x &\equiv 3 \pmod 5 \\
|
||||
x &\equiv 2 \pmod 7 \\
|
||||
\end{aligned}
|
||||
\right.
|
||||
$$
|
||||
|
||||
该问题最早见于《孙子算经》中,并有该问题的具体解法。宋朝数学家秦九韶于 1247 年《数书九章》卷一、二《大衍类》对「物不知数」问题做出了完整系统的解答。上面具体问题的解答口诀由明朝数学家程大位在《算法统宗》中给出:
|
||||
|
||||
> 三人同行七十希,五树梅花廿一支,七子团圆正半月,除百零五便得知。
|
||||
|
||||
$2 \times 70 + 3 \times 21 + 2 \times 15 = 233 = 2 \times 105 + 23$,故答案为 $23$。
|
||||
|
||||
#### CRT概念
|
||||
|
||||
中国剩余定理(Chinese Remainder Theorem, CRT)可以求解如下形式一元线性同余方程组( $n_1, n_2, \dots, n_k$ 两两互质):
|
||||
|
||||
$$
|
||||
\left\{
|
||||
\begin{aligned}
|
||||
x &\equiv a_1 \pmod{n_1} \\
|
||||
x &\equiv a_2 \pmod{n_2} \\
|
||||
&\dots \\
|
||||
x &\equiv a_k \pmod{n_k} \\
|
||||
\end{aligned}
|
||||
\right.
|
||||
$$
|
||||
|
||||
#### CRT过程
|
||||
|
||||
1. 计算所有模数的乘积 $n$
|
||||
2. 对于每一个方程:
|
||||
1. 计算 $m = \frac{n}{n_i}$
|
||||
2. 计算在 $\pmod n_i$ 意义下的 $m_i^{-1}$
|
||||
3. 计算 $c_i = m_i m_i^{-1}$
|
||||
3. 方程组在 $\pmod n$ 意义下的唯一解为 $x = \sum^{k}_{i = 1} a_i c_i \pmod n$
|
||||
|
||||
#### CRT代码
|
||||
|
||||
```cpp
|
||||
LL CRT(int k, LL *a, LL *r)
|
||||
{
|
||||
LL n = 1, ans = 0;
|
||||
for (int i = 1; i <= k; i++)
|
||||
n = n * r[i];
|
||||
for (int i = 1; i <= k; i++)
|
||||
{
|
||||
LL m = n / r[i], b, y;
|
||||
exgcd(m, r[i], b, y); // b * m mod r[i] = 1
|
||||
ans = (ans + a[i] * m * b % n) % n;
|
||||
}
|
||||
return (ans % n + n) % n;
|
||||
}
|
||||
```
|
||||
|
||||
#### 扩展(EXCRT):模数不互质
|
||||
|
||||
##### EXCRT两个方程
|
||||
|
||||
已知两个方程:
|
||||
|
||||
$$
|
||||
x \equiv a_1 \pmod{m_1} \\
|
||||
x \equiv a_2 \pmod {m_2} \\
|
||||
$$
|
||||
|
||||
将其转换为不定方程:
|
||||
|
||||
$$
|
||||
x = m_1p + a_1 = m_2q + a_2
|
||||
$$
|
||||
|
||||
其中 $p, q$ 为整数,则
|
||||
|
||||
$$
|
||||
m_1p - m_2q = a_2 - a_1
|
||||
$$
|
||||
|
||||
根据[裴蜀定理](#前置知识裴蜀定理),当 $a_2 - a_1$ 不能被 $\gcd(a, b)$ 整除,无解。
|
||||
|
||||
否则可以用[扩展欧几里得算法](#扩展欧几里得)解出一组可行解。
|
||||
|
||||
##### EXCRT多个方程
|
||||
|
||||
两两合并即可。
|
||||
|
||||
---
|
||||
|
||||
## 组合数学与矩阵初步
|
||||
|
||||
### 组合数学部分
|
||||
|
||||
#### 组合数的定义
|
||||
|
||||
从 $n$ 个物品选 $m$ 个的方案数
|
||||
|
||||
$$
|
||||
{n \choose m} = \frac{n!}{m!\ (n - m)!}
|
||||
$$
|
||||
|
||||
#### 组合恒等式
|
||||
|
||||
$$
|
||||
{n \choose m} = {n \choose {n - m}} = {\frac {n}{m}}{{n - 1} \choose {m - 1}} = {{n - 1} \choose m} + {{n - 1} \choose {m - 1}}
|
||||
$$
|
||||
|
||||
$$
|
||||
\sum^m_{i = 0}{n \choose i} = {{m + 1} \choose {n + 1}}
|
||||
$$
|
||||
|
||||
$$
|
||||
\sum^k_{i = 0}{n \choose i}{m \choose {k - i}} = {{n + m} \choose k}
|
||||
$$
|
||||
|
||||
#### 二项式定理
|
||||
|
||||
$$
|
||||
(x + y)^n = \sum^n_{k = 0}{n \choose k}x^{k}y^{n - k}
|
||||
$$
|
||||
|
||||
其实就是展开式的通项公式。
|
||||
|
||||
#### 二项式反演
|
||||
|
||||
若已知函数 $g$:
|
||||
|
||||
$$g_n = \sum^{n}_{i = 0} {n \choose i}f_i$$
|
||||
|
||||
则:
|
||||
|
||||
$$f_n = \sum^{n}_{i = 0}(-1)^{n - i}{n \choose i}g_i$$
|
||||
|
||||
证明就算了吧。。。
|
||||
|
||||
### 矩阵部分
|
||||
|
||||
#### 矩阵简介
|
||||
|
||||
矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合,最早来自于方程组的系数以及常数所构成的方阵。
|
||||
|
||||
由 $m \times n$ 个数 $a_{i, j}$ 排成的 $m$ 行 $n$ 列的数表称为 $m$ 行 $n$ 列的矩阵,简称 $m \times n$ 矩阵。记作:
|
||||
|
||||
$$
|
||||
A = \left[
|
||||
\begin{array}{}
|
||||
a_{1, 1} & a_{1, 2} & \dots & a_{1, n} \\
|
||||
a_{2, 1} & a_{2, 2} & \dots & a_{2, n} \\
|
||||
\dots & \dots & \dots & \dots \\
|
||||
a_{m, 1} & a_{m, 2} & \dots & a_{m, n} \\
|
||||
\end{array}
|
||||
\right]
|
||||
$$
|
||||
|
||||
这 $m \times n$ 个数称作矩阵 $A$ 的元素,简称为**元**,数 $a_{i, j}$ 位于矩阵 $A$ 的 $(i, j)$ 元,以数 $a_{i, j}$ 为 $(i, j)$ 元的矩阵记作 $(a_{i, j})$ 或 $(a_{i, j})_{m \times n}$
|
||||
|
||||
#### 矩阵加减法
|
||||
|
||||
只有同型矩阵才支持加减法。
|
||||
|
||||
对应元素相加减即可。
|
||||
|
||||
显然满足交换律,结合律。
|
||||
|
||||
#### 矩阵数乘
|
||||
|
||||
每个元素都乘这个数即可。
|
||||
|
||||
显然满足交换律,结合律和分配律。
|
||||
|
||||
#### 矩阵转置
|
||||
|
||||
矩阵转置即交换每个元素的行与列下标。
|
||||
|
||||
矩阵 $A$ 的转置记作 $A^T$
|
||||
|
||||
$$
|
||||
(A^T)^T = A \\
|
||||
(aA)^T = a(A^T) \\
|
||||
(AB)^T = A^TB^T
|
||||
$$
|
||||
|
||||
(a为常数)
|
||||
|
||||
#### 矩阵乘法
|
||||
|
||||
不同于加减法,矩阵乘法的要求是第一个矩阵的列数等于另一个矩阵的行数。一个 $n \times r$ 的矩阵乘一个 $r \times m$ 的矩阵得到的是一个 $n \times m$ 的矩阵 设
|
||||
|
||||
$$C = AB$$
|
||||
|
||||
则
|
||||
|
||||
$$C_{i, j} = \sum^{r}_{k = 1}a_{i, k}b_{k, j}$$
|
||||
|
||||
也就是说, $C$ 的第 $i$ 行 $j$ 列是由 $A$ 的第 $i$ 行和 $B$ 的第 $j$ 列逐个相乘的和转换而来。
|
||||
|
||||

|
||||
|
||||
显然,矩阵乘法不满足交换律,但是满足结合律。
|
||||
|
||||
#### 行列式
|
||||
|
||||
一个 $n \times n$ 的方阵 $A$ 的行列式记作 $\det(A)$ 或 $|A|$,一个 $2 \times 2$ 矩阵的行列式可以表示如下:
|
||||
|
||||
$$
|
||||
\det
|
||||
\left(
|
||||
\begin{array}{}
|
||||
a & b \\
|
||||
c & d \\
|
||||
\end{array}
|
||||
\right) = ad - bc
|
||||
$$
|
||||
|
||||
##### $n$ 阶行列式
|
||||
|
||||
$$
|
||||
\left|
|
||||
\begin{array}{}
|
||||
a_{1, 1} & a_{1, 2} & \dots & a_{1, n} \\
|
||||
a_{2, 1} & a_{2, 2} & \dots & a_{2, n} \\
|
||||
\dots & \dots & \dots & \dots \\
|
||||
a_{n, 1} & a_{n, 2} & \dots & a_{n, n} \\
|
||||
\end{array}
|
||||
\right| =
|
||||
\sum_{j_1j_2\dots j_n} (i1)^{\gamma(j_1j_2\dots j_n)}a_{1, j_1}a_{2, j2}\dots a_{n, j_n}
|
||||
$$
|
||||
|
||||
其中 $\sum_{j_1j_2\dots j_n}$ 表示对 $1 - n$ 的所有排列枚举求和, $\gamma(j_1j_2\dots j_n)$ 表示 $j_1j_2\dots j_n$ 的逆序对数。
|
||||
|
||||
##### 行列式的意义
|
||||
|
||||
行列式的集合意义就是行列式中的每一行,或者称作行向量,所构成的超平行多面体的有向面积或体积。
|
||||
|
||||

|
||||
|
||||
如图两个向量 $(a_1, a_2), (b_1, b_2)$ 此图的面积即可用
|
||||
|
||||
$$
|
||||
\left[
|
||||
\begin{array}{}
|
||||
a_1 & a_2 \\
|
||||
b_1 & b_2 \\
|
||||
\end{array}
|
||||
\right]
|
||||
$$
|
||||
|
||||
来表示。
|
||||
|
||||

|
||||
|
||||
三维同理。
|
||||
|
||||
### 组合数学与矩阵初步例题
|
||||
|
||||
#### P1962 斐波那契数列
|
||||
|
||||
[题目链接](https://www.luogu.com.cn/problem/P1962)
|
||||
|
||||
求斐波那契数列的第 $n$ 项 $\pmod{1e9 + 7}$
|
||||
|
||||
$n \le 10^{18}$
|
||||
|
||||
##### P1962矩阵解法
|
||||
|
||||
矩阵 $A = \left[\begin{array}{} F_n & F_{n - 1} \end{array} \right], base = \left[\begin{array}{} 1 & 1 \\ 1 & 0 \end{array} \right], A \times base = \left[\begin{array}{} F_n + F_{n - 1} & F_n \end{array} \right] = \left[\begin{array}{} F_{n + 1} & F_n \end{array} \right]$
|
||||
|
||||
所以答案就是 $\left[\begin{array}{} 1 & 1 \end{array} \right] \times base^{n - 2}$ 的第一行第一列,矩阵快速幂即可(同普通快速幂,但是要重载运算符)。
|
||||
|
||||
##### P1962非矩阵解法
|
||||
|
||||
利用斐波那契递推公式:
|
||||
|
||||
$$
|
||||
F_{2n} = F_{n + 1}^2 - F_{n - 1}^2 = (2F_{n - 1} + F_n) \times F_n \\
|
||||
F_{2n + 1} = F_{n + 1}^2 + F_n^2
|
||||
$$
|
||||
|
||||
数学归纳法即可证明。
|
||||
|
||||
直接用这个方法,是会超时的。
|
||||
|
||||
观察发现若要计算 $F_{100}$,则需要计算 $F_{50}, F_{49}$,而这两个数的递推都是 $F_{24}, F_{25}$,所以可以用 map 暂时记忆化即可解决。
|
||||
|
||||
---
|
||||