普通视图

发现新文章,点击刷新页面。
昨天以前ChungZH 的小窝

网络流笔记

2023年7月29日 08:00

最大流

概念

  • 容量:$capacity(e)$ 表示一条有向边 $e(u, v)$ 的最大允许的流量。
  • 流量:$flow(e)$ 表示一条有向边 $e(u, v)$ 总容量中已被占用的流量。
  • 剩余流量(残量):$w(e) = c(e)-f(e)$,表示当前时刻某条有向边 $e(u, v)$ 总流量中未被占用的部分。
  • 反向边:原图中每一条有向边在残量网络中都有对应的反向边,反向边的容量为 $0$,容量的变化与原边相反;『反向边』的概念是相对的,即一条边的反向边的反向边是它本身。
  • 残量网络:在原图的基础之上,添加每条边对应的反向边,并储存每条边的当前流量。残量网络会在算法进行的过程中被修改。
  • 增广路(augmenting path):残量网络中从源点到汇点的一条路径,增广路上所有边中最小的剩余容量为增广流量
  • 增广(augmenting):在残量网络中寻找一条增广路,并将增广路上所有边的流量加上增广流量的过程。
  • 层次:$level(u)$ 表示节点 $u$ 在层次图中与源点的距离。
  • 层次图:在原残量网络中按照每个节点的层次来分层,只保留相邻两层的节点的图,满载(即流量等于容量)的边不存在于层次图中

流的三个重要性质:

  1. 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即,$f(u,v)\leq c(u,v)$
  2. 斜对称性:每条边的流量与其相反边的流量之和为 0,即 $f(u,v)=-f(v,u)$
  3. 流守恒性:从源点流出的流量等于汇点流入的流量,即 $\forall x\in V-{s,t},\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)$

最大流问题:指定合适的流 $f$,以最大化整个网络的流量(即 $\sum_{(s,v)\in E}f(s,v)$)。

Ford-Fulkerson 增广

增广路指一条从 $s$ 到 $t$ 的路径,路径上每条边残余容量都为正。把残余容量为正($w(u, v) \gt 0$)的边设为可行边,然后搜索寻找增广路。

找到一条增广路后,这条路能够增广的流值由路径上边的最小残留容量 $w(u, v)$(记为 $flow$)决定。将这条路径上每条边的 $w(u, v)$ 减去 $flow$。最后,路径上每条边的反向边残留容量要加上 $flow$。

为什么增广路径上每条边的反向边的残留容量值要加上 $flow$?因为斜对称性,残量网络=容量网络-流量网络,容量网络不变时,流量网络上的边的流量增加 $flow$,反向边流量减去 $flow$,残量网络就会发生相反的改变。从另一个角度来说,这个操作可以理解为「退流」,给了我们一个反悔的机会,让增广路的顺序不受限制。

增广路算法好比是自来水公司不断的往水管网里一条一条的通水。

这个算法基于増广路定理:网络达到最大流当且仅当残留网络中没有増广路。

建图技巧:从 $2$ 开始对边编号,这样 $i$ 的反向边就是 $i \oplus 1$。

Edmonds–Karp 算法

用 BFS 找增广路:

  • 如果在 $G_f$ 上我们可以从 $s$ 出发 BFS 到 $t$,则我们找到了新的增广路。
  • 对于增广路 $p$,我们计算出 $p$ 经过的边的剩余容量的最小值 $\Delta = \min_{(u, v) \in p} c_f(u, v)$。我们给 $p$ 上的每条边都加上 $\Delta$ 流量,并给它们的反向边都退掉 $\Delta$ 流量,令最大流增加了 $\Delta$。
  • 因为我们修改了流量,所以我们得到新的 $G_f$,我们在新的 $G_f$ 上重复上述过程,直至增广路不存在,则流量不再增加。

显然,单轮 BFS 增广的时间复杂度是 $O(m)$。增广总轮数的上界是 $O(nm)$。相乘后得到 EK 算法的时间复杂度是 $O(nm^2)$

代码实现中,$w$ 表示边的容量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MAXN = 250, MAXM = 5005;
const ll INF = 0x3f3f3f3f;

struct Edge {
 int from, to;
 ll w;
} edges[2 * MAXM];
int n, m, s, t, tot = 1;
vector<int> G[MAXN]; // G: x 的所有边在 edges 中的下标
ll a[MAXN]; // a: BFS 过程中最近接近点 x 的边给它的最大流
int p[MAXN]; // p: 最近接近 x 的边
void addEdge(int from, int to, ll w) {
 edges[++tot] = {from, to, w};
 edges[++tot] = {to, from, 0};
 G[from].push_back(tot - 1);
 G[to].push_back(tot);
}
bool bfs(int s, int t) {
 memset(a, 0, sizeof a);
 memset(p, 0, sizeof p);
 queue<int> Q;
 Q.push(s);
 a[s] = INF;
 while (!Q.empty()) {
 int x = Q.front();
 Q.pop();
 for (int i = 0; i < G[x].size(); i++) {
 Edge &e = edges[G[x][i]];
 if (!a[e.to] && e.w) {
 p[e.to] = G[x][i]; // G[x][i] 是最接近 e.to 的边
 a[e.to] = min(a[x], e.w); // 最接近 e.to 的边赋给它的流
 Q.push(e.to);
 if (e.to == t) return true;
 }
 }
 }
 return false;
}
ll EK(int s, int t) {
 ll flow = 0;
 while (bfs(s, t)) {
 for (int u = t; u != s; u = edges[p[u]].from) { // 回溯路径
 edges[p[u]].w -= a[t];
 edges[p[u] ^ 1].w += a[t];
 }
 flow += a[t];
 }
 return flow;
}
int main() {
 ios::sync_with_stdio(false);
 cin >> n >> m >> s >> t;
 for (int i = 0; i < m; i++) {
 int u, v;
 ll w;
 cin >> u >> v >> w;
 addEdge(u, v, w);
 }
 cout << EK(s, t) << endl;
 return 0;
}

Dinic 算法

EK 在有些情况中比较低效,我们引入另一个算法:Dinic。

考虑在增广前先对 $G_f$ 做 BFS 分层,即根据结点 $u$ 到源点 $s$ 的距离 $d(u)$ 把结点分成若干层。令经过 $u$ 的流量只能流向下一层的结点 $v$,即删除 $u$ 向层数标号相等或更小的结点的出边,我们称 $G_f$ 剩下的部分为层次图(Level Graph)。形式化地,我们称 $G_L = (V, E_L)$ 是 $G_f = (V, E_f)$ 的层次图,其中 $E_L = \left{ (u, v) \mid (u, v) \in E_f, d(u) + 1 = d(v) \right}$。

如果我们在层次图 $G_L$ 上找到一个最大的增广流 $f_b$,使得仅在 $G_L$ 上是不可能找出更大的增广流的,则我们称 $f_b$ 是 $G_L$ 的阻塞流(Blocking Flow)。

定义层次图和阻塞流后,Dinic 算法的流程如下。

  1. 在 $G_f$ 上 BFS 出层次图 $G_L$。
  2. 在 $G_L$ 上 DFS 出阻塞流 $f_b$。
  3. 将 $f_b$ 并到原先的流 $f$ 中,即 $f \leftarrow f + f_b$。
  4. 重复以上过程直到不存在从 $s$ 到 $t$ 的路径。

此时的 $f$ 即为最大流。

我们还需要当前弧优化。注意到在 $G_L$ 上 DFS 的过程中,如果结点 $u$ 同时具有大量入边和出边,并且 $u$ 每次接受来自入边的流量时都遍历出边表来决定将流量传递给哪条出边,则 $u$ 这个局部的时间复杂度最坏可达 $O(|E|^2)$。事实上,如果我们已经知道边 $(u, v)$ 已经增广到极限(边 $(u, v)$ 已无剩余容量或 $v$ 的后侧已增广至阻塞),则 $u$ 的流量没有必要再尝试流向出边 $(u, v)$。据此,对于每个结点 $u$,我们维护 $u$ 的出边表中第一条还有必要尝试的出边。习惯上,我们称维护的这个指针为当前弧,称这个做法为当前弧优化。

将单轮增广的时间复杂度 $O(nm)$ 与增广轮数 $O(n)$ 相乘,Dinic 算法的时间复杂度是 $O(n^2m)$。

对于 Dinic 算法的复杂度,有如下 $3$ 种情况:

  • 一般的网络图:$O(n^2m)$
  • 单位容量的图:$O(\min(\sqrt m,n^{\frac{2}{3}})\cdot m)$
  • 二分图:$O(m\sqrt n)$

(Dinic 中 DFS 最好要写 vis,因为求费用流一定要)

错误:有一次漏判了 dep[v] == dep[u] + 1!玄学:若 dep 默认值为 $0$,则一定要 $dep[S]=1$!!!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <bits/stdc++.h>
using namespace std;

using ll = long long;
const int MAXN = 250, MAXM = 5005;
const ll INF = 0x3f3f3f3f;

struct Edge {
 int to, nxt;
 ll cap, flow;
} e[2 * MAXM];
int fir[MAXN];
int n, m, s, t, tot = 1;
int dep[MAXN], cur[MAXN]; // cur 记录当前弧
bool vis[MAXN];
void addEdge(int from, int to, ll w) {
 e[++tot] = {to, fir[from], w, 0};
 fir[from] = tot;
 e[++tot] = {from, fir[to], 0, 0};
 fir[to] = tot;
}
bool bfs(int s, int t) {
 queue<int> Q;
 memset(dep, 0, sizeof dep);
 dep[s] = 1;
 Q.push(s);
 while (!Q.empty()) {
 int x = Q.front();
 Q.pop();
 for (int i = fir[x]; ~i; i = e[i].nxt) {
 int v = e[i].to;
 if (!dep[v] && e[i].cap > e[i].flow) {
 dep[v] = dep[x] + 1;
 Q.push(v);
 }
 }
 }
 return dep[t];
}
ll dfs(int u, int t, ll flow) {
 if (u == t) return flow;
 ll ans = 0;
 vis[u] = 1;
 for (int &i = cur[u]; ~i; i = e[i].nxt) { // i 用引用:当前弧优化
 int v = e[i].to;
 if (!vis[v] && dep[v] == dep[u] + 1 && e[i].cap > e[i].flow) {
 ll d = dfs(v, t, min(flow - ans, e[i].cap - e[i].flow));
 ans += d;
 e[i].flow += d;
 e[i ^ 1].flow -= d;
 if (ans == flow) break; // 剪枝,残余流量用尽,停止增广
 }
 }
 vis[u] = 0;
 return ans;
}
ll Dinic(int s, int t) {
 ll ans = 0;
 while (bfs(s, t)) {
 memcpy(cur, fir, sizeof cur); // 当前弧优化
 ans += dfs(s, t, INF);
 }
 return ans;
}
int main() {
 ios::sync_with_stdio(false);
 cin >> n >> m >> s >> t;
 memset(fir, -1, sizeof fir);
 for (int i = 0; i < m; i++) {
 int u, v;
 ll w;
 cin >> u >> v >> w;
 addEdge(u, v, w);
 }
 cout << Dinic(s, t) << endl;
 return 0;
}

最小割

概念

  • 割:对于一个网络流图 $G=(V,E)$,其割的定义为一种 点的划分方式:将所有的点划分为 $S$ 和 $T=V-S$ 两个集合,其中源点 $s\in S$,汇点 $t\in T$。
  • 割的容量:我们的定义割 $(S,T)$ 的容量 $c(S,T)$ 表示所有从 $S$ 到 $T$ 的边的容量之和,即 $c(S,T)=\sum_{u\in S,v\in T}c(u,v)$。当然我们也可以用 $c(s,t)$ 表示 $c(S,T)$。
  • 最小割:最小割就是求得一个割 $(S,T)$ 使得割的容量 $c(S,T)$ 最小。

最大流最小割定理

证明:

定理:$f(s,t){\max}=c(s,t){\min}$

对于任意一个可行流 $f(s,t)$ 的割 $(S,T)$,我们可以得到:

$$ f(s,t)=S\text{出边的总流量}-S\text{入边的总流量}\le S\text{出边的总流量}=c(s,t) $$

如果我们求出了最大流 $f$,那么残余网络中一定不存在 $s$ 到 $t$ 的增广路经,也就是 $S$ 的出边一定是满流,$S$ 的入边一定是零流,于是有:

$$ f(s,t)=S\text{出边的总流量}-S\text{入边的总流量}=S\text{出边的总流量}=c(s,t) $$

结合前面的不等式,我们可以知道此时 $f$ 已经达到最大。

实现

最小割:最大流

方案:我们可以通过从源点 $s$ 开始 DFS,每次走残量大于 $0$ 的边,找到所有 $S$ 点集内的点。

最小割边数量:如果需要在最小割的前提下最小化割边数量,那么先求出最小割,把没有满流的边容量改成 $\infty$,满流的边容量改成 $1$,重新跑一遍最小割就可求出最小割边数量;如果没有最小割的前提,直接把所有边的容量设成 $1$,求一遍最小割就好了。

模型一:二者取其一问题

有 $n$ 个物品和两个集合 $A,B$,如果一个物品没有放入 $A$ 集合会花费 $a_i$,没有放入 $B$ 集合会花费 $b_i$;还有若干个形如 $u_i,v_i,w_i$ 限制条件,表示如果 $u_i$ 和 $v_i$ 同时不在一个集合会花费 $w_i$。每个物品必须且只能属于一个集合,求最小的代价。

这是一个经典的 二者选其一 的最小割题目。我们对于每个集合设置源点 $s$ 和汇点 $t$,第 $i$ 个点由 $s$ 连一条容量为 $a_i$ 的边、向 $t$ 连一条容量为 $b_i$ 的边。对于限制条件 $u,v,w$,我们在 $u,v$ 之间连容量为 $w$ 的双向边。

注意到当源点和汇点不相连时,代表这些点都选择了其中一个集合。如果将连向 $s$ 或 $t$ 的边割开,表示不放在 $A$ 或 $B$ 集合,如果把物品之间的边割开,表示这两个物品不放在同一个集合。

最小割就是最小花费。

模型二:最大权值闭合图

最大权值闭合图,即给定一张有向图,每个点都有一个权值(可以为正或负或 $0$),你需要选择一个权值和最大的子图,使得子图中每个点的后继都在子图中。

做法:建立超级源点 $s$ 和超级汇点 $t$,若节点 $u$ 权值为正,则 $s$ 向 $u$ 连一条有向边,边权即为该点点权;若节点 $u$ 权值为负,则由 $u$ 向 $t$ 连一条有向边,边权即为该点点权的相反数。原图上所有边权改为 $\infty$。跑网络最大流,将所有正权值之和减去最大流,即为答案。

几个小结论来证明:

  1. 每一个符合条件的子图都对应流量网络中的一个割。因为每一个割将网络分为两部分,与 $s$ 相连的那部分满足没有边指向另一部分,于是满足上述条件。这个命题是充要的。
  2. 最小割所去除的边必须与 $s$ 和 $t$ 其中一者相连。因为否则边权是 $\infty$,不可能成为最小割。
  3. 我们所选择的那部分子图,权值和 $=$ 所有正权值之和 $-$ 我们未选择的正权值点的权值之和 $+$ 我们选择的负权值点的权值之和。当我们不选择一个正权值点时,其与 $s$ 的连边会被断开;当我们选择一个负权值点时,其与 $t$ 的连边会被断开。断开的边的边权之和即为割的容量。于是上述式子转化为:权值和 $=$ 所有正权值之和 $-$ 割的容量。
  4. 于是得出结论,最大权值和 $=$ 所有正权值之和 $-$ 最小割 $=$ 所有正权值之和 $-$ 最大流。

P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查 Title

小M的作物Title

最小费用最大流

费用流

给定一个网络 $G=(V,E)$,每条边除了有容量限制 $c(u,v)$,还有一个单位流量的费用 $w(u,v)$。

当 $(u,v)$ 的流量为 $f(u,v)$ 时,需要花费 $f(u,v)\times w(u,v)$ 的费用。

$w$ 也满足斜对称性,即 $w(u,v)=-w(v,u)$。

则该网络中总花费最小的最大流称为 最小费用最大流,即在最大化 $\sum_{(s,v)\in E}f(s,v)$ 的前提下最小化 $\sum_{(u,v)\in E}f(u,v)\times w(u,v)$。

SSP 算法

SSP(Successive Shortest Path)算法是一个贪心的算法。它的思路是每次寻找单位费用最小的增广路进行增广,直到图上不存在增广路为止。

如果图上存在单位费用为负的圈,SSP 算法无法正确求出该网络的最小费用最大流。此时需要先使用消圈算法消去图上的负圈。

时间复杂度: 如果使用 Bellman–Ford 求解最短路,每次找增广路的时间复杂度为 $O(nm)$。设该网络的最大流为 $f$,则最坏时间复杂度为 $O(nmf)$。事实上,SSP 算法是伪多项式时间的。

实现

可以在 Dinic 基础上改进,将 BFS 求分层图换为 SPFA(有负权边不能用 Dijkstra)求一条单位费用之和最小的路径,也就是把 $w(u, v)$ 当做边权然后在残量网络上求最短路,在 DFS 中一定要用 vis 数组记录点是否访问过。这样就可以求得最小费用最大流了。

建反向边时,费用取反即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <bits/stdc++.h>
using namespace std;

using ll = long long;
const int MAXN = 100055, MAXM = 100005;
const int INF = 0x3f3f3f3f;

struct Edge {
 int to, nxt, cap, cost;
} e[2 * MAXM];
int fir[MAXN];
int n, m, s, t, tot = 1;
int dis[MAXN];
bool vis[MAXN];
int cur[MAXN]; // cur 记录当前弧
void addEdge(int from, int to, int w, int cost) {
 e[++tot] = {to, fir[from], w, cost};
 fir[from] = tot;
 e[++tot] = {from, fir[to], 0, -cost};
 fir[to] = tot;
}
bool spfa(int s, int t) {
 memset(dis, 0x3f, sizeof dis);
 queue<int> q;
 q.push(s), dis[s] = 0, vis[s] = 1;
 while (!q.empty()) {
 int f = q.front();
 q.pop();
 vis[f] = 0;
 for (int i = fir[f]; ~i; i = e[i].nxt) {
 int v = e[i].to;
 if (e[i].cap && dis[f] + e[i].cost < dis[v]) {
 dis[v] = dis[f] + e[i].cost;
 if (!vis[v]) {
 vis[v] = 1;
 q.push(v);
 }
 }
 }
 }
 return dis[t] < INF;
}
int dfs(int u, int t, int flow) {
 if (u == t) return flow;
 int ans = 0;
 vis[u] = 1;
 for (int &i = cur[u]; ~i; i = e[i].nxt) { // i 用引用:当前弧优化
 int v = e[i].to;
 if (!vis[v] && dis[v] == dis[u] + e[i].cost && e[i].cap) {
 int d = dfs(v, t, min(flow - ans, e[i].cap));
 ans += d;
 e[i].cap -= d;
 e[i ^ 1].cap += d;
 // 易错:这里不写 return ans,应为 break,需要执行下面的 vis[u] = 0
 if (ans == flow) break; // 剪枝,残余流量用尽,停止增广
 }
 }
 vis[u] = 0;
 return ans;
}
pair<int, int> Dinic(int s, int t) {
 int maxFlow = 0, minCost = 0;
 while (spfa(s, t)) {
 memcpy(cur, fir, sizeof cur); // 当前弧优化
 int x = dfs(s, t, INF);
 maxFlow += x;
 minCost += x * dis[t];
 }
 return make_pair(maxFlow, minCost);
}
int main() {
 cin >> n >> m >> s >> t;
 memset(fir, -1, sizeof fir);
 for (int i = 0; i < m; i++) {
 int u, v, w, c;
 cin >> u >> v >> w >> c;
 addEdge(u, v, w, c);
 }
 pair<int, int> ans = Dinic(s, t);
 cout << ans.first << " " << ans.second << endl;
 return 0;
}

参考资料

网络流题集

2023年7月29日 08:00

最大流

Luogu-P1231 教辅的组成

三倍经验: Luogu-P1402 酒店之王 / Luogu-P2891 [USACO07OPEN] Dining G

一眼丁真建图:S->练习册->书->答案->T

然而是错的。很明显,书有可能被多次匹配,与题意不符。

正确的建图:S->练习册->书(拆点)->答案->T

为什么中间层的书要拆点呢?因为一本书不能被重复选用。我们的目的是保证一本书流出的流量只能是 $1$。所以我们把每个代表书的点拆成两个点,左边的点和练习册连边,右边的点和答案连边;左右对应点之间也要连一条容量为 $1$ 的边。

Luogu-P2764 最小路径覆盖问题

定理:最小路径覆盖数=$|G|$-二分图最大匹配数

首先我们假设现在原图内每个点都是一条路径,此时最少路径数为 $n$。

考虑合并路径,当且仅当两条路径首尾相连的时候可以合并。

将点 $x$ 拆成出点 $x$ 和入点 $x+n$,当我们连接 $u, v$ 时,转化为连接 $u, v+n$。将 $S$ 与所有 $u$ 连边,将所有 $u+n$ 与 $T$ 连边。所有边的容量都为 $1$。

在一开始每个点都是一条独立的路径,每次合并将两条路径合并为一条路径,那么最终路径即为点数减去最大匹配数,这样求得的路径覆盖即为最小路径覆盖。

对于输出路径,用 to 记录下一个节点,tag 标记该节点前面是否还有点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
ll dfs(int u, int t, ll flow) {
 if (u == t) return flow;
 ll ans = 0;
 vis[u] = 1;
 for (int &i = cur[u]; ~i; i = e[i].nxt) { // i 用引用:当前弧优化
 int v = e[i].to;
 if (!vis[v] && dep[v] == dep[u] + 1 && e[i].cap > e[i].flow) {
 ll d = dfs(v, t, min(flow - ans, e[i].cap - e[i].flow));
 if (!d) continue;
 ans += d;
 e[i].flow += d;
 e[i ^ 1].flow -= d;
 to[u] = v;
 if (u != S) { tag[v - n] = 1; }
 if (ans == flow) break; // 剪枝,残余流量用尽,停止增广
 }
 }
 vis[u] = 0;
 return ans;
}
ll Dinic(int s, int t) {
 ll ret = 0;
 while (bfs(s, t)) {
 memcpy(cur, fir, sizeof cur);
 ret += dfs(s, t, INF);
 }
 for (int i = 1; i <= n; i++) {
 if (tag[i] == 0) {
 cout << i << " ";
 int x = i;
 while (to[x] && to[x] != t) {
 cout << to[x] - n << " ";
 x = to[x] - n;
 }
 cout << "\n";
 }
 }
 return ret;
}
int main() {
 ios::sync_with_stdio(false);

 memset(fir, -1, sizeof fir);
 cin >> n >> m;
 S = 2 * n + 1, T = S + 1;
 for (int i = 0; i < m; i++) {
 int x, y;
 cin >> x >> y;
 addEdge(x, y + n, 1);
 }
 for (int i = 1; i <= n; i++) {
 addEdge(S, i, 1);
 addEdge(i + n, T, 1);
 }
 int t = Dinic(S, T);
 cout << n - t << endl;
 return 0;
}

最小割

方格取数问题

对矩阵进行黑白染色,那么比如选了一个黑格,那么与它相邻的白格不能选。可以计算不选的方格,其余的都选。容易想到最小割。

那么建图就很容易了。对于每个点 $(i,j)$,数值为 $a_{i,j}$,如果是白色点,那么从源点向它连一条容量 $a_{i,j}$ 的边;否则就从这个点向汇点连一条容量 $a_{i,j}$ 的边。这样我们就把不选的点转化成了割掉的边。为了保证相邻点直接的边不会被割掉,我们从每个白点向与其相邻的黑点连容量为 $INF$ 的边(他们之间的边只从白连到黑,避免重复计算)。答案为全局和减去最小割。

Luogu-P2598 [ZJOI2009] 狼和羊的故事

原点向所有狼连容量 $INF$ 的边,所有羊向汇点连容量 $INF$ 的边,所有点向四周连容量为 $1$ 的边。

最小割即为答案,因为狼和羊之间的边都被割掉了。

Luogu-P1361 小M的作物

二者取其一模型。

考虑点集 ${u, v, w}$ 对集合 $A$ 的贡献,加入其中一者被割进 $B$,那这个点集就没有贡献,即点集与 $A$ 的连边要割掉。

用一个虚点 $x$ 从 $S$ 连一条边代表贡献,如果其中一个点被割进了集合 $B$,那这条代表贡献的边也要割掉,而 $x$ 到 $u, v, w$ 的边不能断开。所以 $(S, x)$ 的容量为 $c$,$(x, u), (x, v), (x, w)$ 的容量都是 $INF$(保证不被断开)。

答案为总收益减去最小割。

Luogu-P2762 太空飞行计划问题

两倍经验:Luogu-P3410 拍照

最大权值闭合图模型。

Luogu-P4177 [CEOI2008] order

如果没有租机器,那这道题就是纯最大权值闭合图模型。

图中对于工作和它所需的机器之间边的容量为 $INF$,所以这条边不可能被割,意义即为选择这个工作就必须购买这个机器。那么租用就可以将这条边的容量改为 $b_{ij}$,可以用 $b_{ij}$ 割掉这条边,表示选择这个工作后,可以花费 $b_{ij}$ 的代价代替购买机器。

初等数论入门

2023年5月5日 08:00

我也不知道这是从哪本书上抠来的?

整除

定义 1:如果 $a$ 和 $b$ 为整数且 $a \ne 0$,我们说 $a$ 整除 $b$ 是指存在整数 $c$ 使得 $b=ac$。如果 $a$ 整除 $b$,我们还称 $a$ 是 $b$ 的一个因子,且称 $b$ 是 $a$ 的倍数

如果 $a$ 整除 $b$,则将其记为 $a \mid b$,如果 $a$ 不能整除 $b$,则记其为 $a \nmid b$。

定理 1:如果 $a, b$ 和 $c$ 是整数,且 $a \mid b, b \mid c$,则 $a \mid c$。

定理 2:如果 $a, b, m$ 和 $n$ 为整数,且 $c \mid a, c \mid b$,则 $c \mid (ma+nb)$。

定理 3带余除法 如果 $a$ 和 $b$ 是整数且 $b \gt 0$,则存在唯一的整数 $q$ 和 $r$,使得$a = bq + r, 0 ≤ r < b$。

最大公因子及其性质

定义 2:不全为零的整数 $a$ 和 $b$ 的最大公因子是指能够同时整除 $a$ 和 $b$ 的最大整数。

定义 3:设 $a,b$ 均为非零整数,如果 $a$ 和 $b$ 最大公因子 $(a,b)=1$,则称 $a$ 与 $b$ 互素。

定理 4:$a,b$ 是整数,且 $(a, b)=d$,那么 $(a/d, b/d)=1$。(换言之,$a/d$ 与 $b/d$ 互素)

推论 1:如果 $a, b$ 为整数,且 $b\ne 0$,则 $a/b=p/q$,其中 $p, q$ 为整数,且 $(p,q)=1, q≠0$。

定理 5:令 $a, b, c$ 是整数,那么 $(a+cb, b) = (a, b)$

证明:令 $a, b, c$ 是整数,证明 $a, b$ 的公因子与 $a+cb, b$ 的公因子相同,即证明 $(a+cb, b)=(a, b)$。

令 $e$ 是 $a, b$ 的公因子,由定理 2 可知 $e \mid (a+cb)$,所以 $e$ 是 $a+cb$ 和 $b$ 的公因子。

如果 $f$ 是 $a+cb$ 和 $b$ 的公因子,由定理 2 可知 $f$ 整除 $(a+cb)-cb=a$,所以 $f$ 是 $a, b$ 的公因子,因此 $(a+cb, b)=(a,b)$。

定义 4线性组合 如果 $a,b$ 是整数,那么它们的线性组合具有形式 $ma+nb$,其中 $m,n$ 都是整数。

定理 6:两个不全为零的整数 $a, b$ 的最大公因子是 $a, b$ 的线性组合中最小的正整数。

证明:令 $d$ 是 $a,b$ 的线性组合中最小的正整数,$d = ma + nb$,其中 $m,n$ 是整数,我们将证明 $d\mid a, d\mid b$。

由带余除法,得到 $a=dq+r, 0\le r\lt d$。

由 $a=dq+r $和 $d=ma+nb$,得到 $r=a-dq=a-q(ma+nb)=(1-qm)a-qnb$。

这就证明了整数 $r$ 是 $a,b$ 的线性组合。因为 $0 \le r \lt d$,而 $d$ 是 $a,b$ 的线性组合中最小的正整数,

于是我们得到 $r=0$(如果 $r$ 不是等于 $0$,那意味着 $r$ 才是所有线性组合中最小的正整数,这与 $d$ 是所有线性组合中最小的正整数矛盾),因此 $d\mid a$,同理可得,$d\mid b$。

我们证明了 $a,b$ 的线性组合中最小的正整数 $d$ 是 $a,b$ 的公因子,剩下要证的是它是 $a,b$ 的最大公因子,为此只需证明 $a,b$ 所有的公因子都能整除 $d$。

由于 $d = ma + nb$,因此如果 $c \mid a$ 且 $c \mid b$,那么由定理 2 有 $c \mid d$,因此 $d \gt c$,这就完成了证明。

定义 5:令 $a_1,a_2,…,a_n$ 是不全为零的整数,这些整数的公因子中最大的整数就是最大公因子。$a_1,a_2,…,a_n$ 的最大公因子记为 $(a_1, a_2, …, a_n)$。

引理 1:如果 $a_1,a_2,…,a_n$ 是不全为零的整数,那么 $(a_1, a_2, …, a_{n-1}, a_n) = (a_1, a_2, …, (a_{n-1}, a_n))$。

欧几里得算法

辗转相除法求 gcd,即 $\gcd(a, b) = \gcd(b, a\bmod b)$。

证明 1:由定理 3 带余除法,存在整数 $q, r$ 使得 $a = bq + r, 0 \le r \lt b$, 得到 $r = a - bq$。由定理 5 得,$\gcd(a,b) = \gcd(a-bq,b) = \gcd(r,b) = \gcd(a%b,b) = \gcd(b,a%b)$。

证明 2:令 $d=(a,b)$,证明 $d \mid (b,a\bmod b)$,再反证 $(b,a\bmod b) \gt d$ 是不可能的。

时间复杂度的证明:

假设 $a\gt b$,分两种情况:

  1. $b \lt a/2$, 经过一次辗转得到 $(b,a\bmod b)$,$\max(a,b)$ 至少缩小一半。
  2. $b \ge a/2$,经过两次辗转得到彼时的 $\max(a,b)$ 至少缩小一半。

综上所述,最多 $2\log(n)$ 次辗转算法结束。

裴蜀定理

如果 $a$ 与 $b$ 均为整数,则存在整数 $x$ 和 $y$ 满足 $ax + by = (a,b)$。

由定理 6 容易证明正确性。

下面用扩展欧几里得算法(exgcd)求出满足 $ax + by = (a,b)$ 的一个特解。

例如:$a = 99, b = 78$, 令 $d =(a,b) = (99,78) = 3$,求 $99x + 78y = 3$ 的一个特解。

在调用 exgcd 的时候,从后往前依次构造出相应的解。

$a$ $b$ $d$ $x$ $y$ 备注
$99$ $78$ $3$ $-11$ $14$ 怎样由 $78x + 21y = 3$的一个特解 $x=3, y=-11$,构造出 $99x + 78y = 3$ 的一个特解?
$78$ $21$ $3$ $3$ $-11$ $78x + 21y = 3$ 的一个特解 $x=3, y=-11$
$21$ $15$ $3$ $-2$ $3$ $21x + 15y = 3$ 的一个特解 $x=-2, y=3$
$15$ $6$ $3$ $1$ $-2$ $15x + 6y = 3$ 的一个特解 $x=1, y=-2$
$6$ $3$ $3$ $0$ $1$ $6x + 3y = 3$ 的一个特解是 $x=0, y=1$
$3$ $0$ $3$ $1$ $0$ $3x + 0y = 3$ 的一个特解是 $x=1, y=0$

在用欧几里得算法求 $(99,78)$ 的时候,是要先求 $(78,21)$。

假如已经求出 $78x + 21y = 3$ 的一个解 ${x_0,y_0} = {3,-11}$,即 $78x_0 + 21y_0 = 3$。

那么可以由 $78x_0 + 21y_0 = 3$,构造出 $99x + 78y = 3$ 的一个特解。

因 $a=99, b=78, a\bmod b=21$, 因此 $78x_0 + 21y_0 = 3$,可以写成:$bx_0 + (a\bmod b)y_0 = 3$,即 $bx_0+(a-\frac{a}{b}b)y_0=3$,即 $ay_0+b(x_0-\frac{a}{b}y_0)=3$,即 $99y_0+78(x_0-\frac{99}{78}y_0)=3$。

那么只需要令 $x = y_0 = -11, y = x_0 - \frac{99}{78}y_0=14$,就可以得到 $99x + 78y = 3$ 的一个特解,即 ${-11, 14}$ 是 $99x+78y=3$ 的一个特解。

也就是说,在用欧几里得算法求 $(78,21)$ 的时候,若能返回 ${x_0,y_0}$ 使得满足 $78x_0 + 21y_0 = 3$,那么就能构造出一个特解 ${x,y}$ 满足 $99x + 78y = 3$ 的一个特解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void exgcd(int a, int b, int &d, int &x, int &y) {
 if (!b) {
 d = a;
 x = 1;
 y = 0;
 return;
 }
 exgcd(b, a % b, d, x, y);
 int t = x;
 x = y;
 y = t - (a / b) * y;
 return;
}

注意:若 $a\lt 0$ 且 $b\ge 0$ 那么在求 $ax+by=(a,b)$ 的时候,可以先求出 $|a|x+by=(|a|,b)$ 的一组解 ${x_0,y_0}$,然后 ${-x_0,y_0}$ 就是原方程的一个解。

若 $a\ge 0$ 且 $b\lt 0$ 的同理。若 $a \lt 0$ 且 $b \lt 0$ 的也同理。

定理 7:如果 $a,b$ 是正整数,那么所有 $a,b$ 的线性组合构成的集合与所有 $(a,b)$ 的倍数构成的集合相同。

证明:假设 $d = (a,b)$。

  1. 首先证明每个 $a,b$ 的线性组合是 $d$ 的倍数。首先注意到由最大公因子的定义,有 $d\mid a$ 且 $d\mid b$,每个 $a,b$ 的线性组合具有形式 $ma+nb$,其中 $m,n$ 是整数。

由定理 2,只要 $m,n$ 是整数,$d$ 就整除 $ma+nb$,因此,$ma+nb$ 是 $d$ 的倍数。

  1. 现在证明每一个 $d$ 的倍数也是 $(a,b)$ 的线性组合。

由定理 6,存在整数 $r,s$ 使得 $(a,b) = ra + sb$。而 $d$ 的倍数具有形式 $jd$,其中 $j$ 是整数。

在方程 $d = ra + sb$ 的两边同时乘以 $j$,我们得到 $jd = (jr)a + (js)b$。

因此,每个 $d$ 的倍数是 $(a,b)$ 的线性组合。

推论 2:整数 $a$ 与 $b$ 互素当且仅当存在整数 $m$ 和 $n$ 使得 $ma + nb = 1$。

证明:如果 $a,b$ 互素,那么 $(a,b)=1$,由定理 6 可知,$1$ 是 $a$ 和 $b$ 的线性组合的最小正整数,于是存在整数 $m,n$ 使得 $ma + nb = 1$。

反之,如果有整数 $m$ 和 $n$ 使得 $ma + nb = 1$,则由定理 6 可得 $(a,b)=1$,这是由于 $a,b$ 不同为 $0$ 且 $1$ 显然是 $a,b$ 的线性组合中的最小正整数。

二元一次不定方程

引理 2:如果 $a, b, c$ 是正整数,满足 $(a, b) = 1, a \mid bc$,则 $a \mid c$。

证明:由于 $(a, b)=1$,存在整数 $x$ 和 $y$ 使得 $ax+by=1$。等式两边同时乘以$c$,得 $acx+bcy=c$。

根据定理 2,$a$ 整除 $(cx)a + y(bc)$,这是因为这是 $a$ 和 $bc$ 的线性组合,而它们都可以被 $a$ 整除。因此,$a \mid c$。

定理 8:设 $a,b$ 是整数且 $d=(a,b)$。如果 $d \nmid c$,那么方程 $ax+by=c$ 没有整数解,如果 $d \mid c$,那么存在无穷多个整数解。

另外,如果 $x = x_0, y = y_0$ 是方程的一个特解,那么所有的解可以表示为:$x = x_0 + (b/d)n, y = y_0 - (a/d)n$,其中 $n$ 是整数。

证明:由定理 7 可知,$ax+by$ 的结果是 $d$ 的倍数,因此如果 $d \nmid c$,那么方程 $ax+by=c$ 没有整数解。

如果 $d\mid c$,存在整数 $s, t$ 使得 $as+bt=d$。

因为 $d\mid c$,存在整数 $e$ 使得 $de = c, c = de = (as+bt)e = a(se)+b(te)$

因此 $x_0 = se, y_0 = te$ 是方程 $ax + by = c$ 的一个特解。

为了证明方程存在无穷多个解,令 $x = x_0 + (b/d)n, y = y_0 - (a/d)n$,其中 $n$ 是整数。

  1. 证明任何一对整数 $(x_0+(b/d)n, y_0 - (a/d)n)$ 它是方程的解。

因为 $a(x_0+(b/d)n) + b(y_0 - (a/d)n) = ax_0 + by_0 + a(b/d)n - b(a/d)n = ax_0 + by_0$,而 $ax_0+by_0$ 是方程 $ax+by=c$ 的解,所以 $(x_0+(b/d)n, y_0 -(a/d)n)$ 就是方程的解。

  1. 证明方程的任何一个解都具有 $(x_0 + (b/d)n, y_0 - (a/d)n)$ 这种形式。

假设整数 $x,y$ 满足 $ax+by=c$,又因为 $ax_0+by_0=c$,两式相减得到:$a(x-x_0)+b(y-y_0)=0$。

即 $a(x-x_0) = b(y_0-y)$,等式两边同时除以 $d$ 得到 $(a/d)(x-x_0) = (b/d)(y_0-y)$,根据定理 4,$a/d$ 与 $b/d$ 互质,再根据引理 2,$(a/d) \mid (y_0-y)$,因此存在整数 $n$ 使得 $(a/d)n = y_0 - y$,于是得到 $y=y_0-(a/d)n$。

同理可得,$(b/d) \mid (x-x_0)$,因此存在整数 $n$ 使得 $(b/d)n = x - x_0$,于是得到 $x=x_0+(b/d)n$。

习题

  1. Luogu-P5656 【模板】二元一次不定方程 (exgcd) Orz 离散小波变换°

下面设 $p = b/d, q = a/d$。

有正整数解

  • 解的个数:不断地将 $y_0$ 减去 $q$,$x_0$ 加上 $p$ 就可以找到所有可行解,个数为 $\lfloor (y-1)/q\rfloor + 1$
  • $x$ 的最小正整数值:exgcd 得到的 $x_0$
  • $y$ 的最小正整数值:不断地将 $y_0$ 减去 $q$(因为 $x_0$ 最小时,$y_0$ 一定最大),答案为 $(y-1)\bmod q+1$(特别注意 $0$ 的情况)
  • $x$ 的最大正整数值:不断将 $x_0$ 加上 $p$,答案为 $x+\lfloor (y-1)/q\rfloor * p$
  • $y$ 的最大正整数值:$x_0$ 为最小正整数时,$y_0$ 就是最大值

无正整数解

  • $x$ 的最小正整数值:exgcd 得到的 $x_0$
  • $y$ 的最小正整数值:当前的 $y_0 \le 0$,需要执行构造 $x_0$ 的方法,即 $y_0 + q * \lceil (1.0-y)/q \rceil$
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <bits/stdc++.h>
using namespace std;
#define LL long long
void exgcd(LL a, LL b, LL &d, LL &x, LL &y) {
 if (b == 0) {
 d = a;
 x = 1;
 y = 0;
 return;
 }
 exgcd(b, a%b, d, x, y);
 LL t = x;
 x = y;
 y = t-(a/b)*y;
}
int main()
{
 int t;
 cin >> t;
 while (t--) {
 LL a, b, c, x, y, d;
 cin >> a >> b >> c;
 exgcd(a, b, d, x, y);
 if (c%d != 0) {
 puts("-1");
 } else {
 x *= c/d;
 y *= c/d;
 LL p = b/d, q = a/d, k;
 if (x < 0) { k = ceil((1.0-x)/p); x += p*k; y -= q*k; }
 if (x >= 0) { k = (x-1)/p; x -= p*k; y += q*k; }
 if (y > 0) {
 cout << (y-1)/q+1 << " " << x << " " << (y-1)%q+1 << " " << x+(y-1)/q*p << " " << y << endl;
 } else {
 cout << x << " " << y+q*(LL)ceil((1.0-y)/q) << endl;
 }
 }
 }
 return 0;
}
  1. 多元一次不定方程的一组解:求 $a_1x_1 + a_2x_2 + a_3x_3 + … + a_nx_n = c$ 的一组整数解,如无整数解输出 $-1$。

先考虑三元一次不定方程 $a_1x_1 + a_2x_2 + a_3+x_3 = c$。可以用 exgcd 解二元一次方程 $a_1x_1 + a_2x_2 = (a_1, a_2)$,设 $d = (a_1, a_2)$,由定理 8 知道 $d$ 乘任何整数时该方程都有整数解,就可以把 $d$ 当做原方程的一个系数,转而求解 $dt+a_3x_3 = c$。上一步的方程变为 $t(a_1x_1 + a_2x_2) = td$,于是将 $t$ 乘上前面的 $x_1, x_2$ 就得到最终答案。

多元同理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int main()
{
 exgcd(a[1], a[2], ta[2], ans[1], ans[2]);
 int tm = 1;
 for (int i = 3; i <= n; i++) {
 LL tmp;
 exgcd(ta[i-1], a[i], ta[i], tmp, ans[i]);
 if (i == n && c%ta[i]) { cout << -1 << endl; return 0; }
 for (int j = 1; j < i; j++) ans[j] *= tmp;
 }
 tm = c/ta[n];
 for (int i = 1; i <= n; i++) {
 cout << ans[i]*tm <<" ";
 }
 return 0;
}
  1. Luogu-P3986 斐波那契数列:求 $f(i)x+f(i+1)y=k$ 的正整数解个数($f$ 表示斐波那契数列)

显然 $f(i)+f(i+1)\gt k$ 时就不用继续下去了,因此方程的个数是有限的,可以枚举。

然后题目就变成了求 $f_ia+f_{i+1}b=k$ 的正整数解的个数。

另外,有没有可能同样的 $(a, b)$ 出现了两次呢?不可能。否则就需要满足 $af_i+bf_{i+1}=af_j+bf_{j+1}, i \ne j$,然而斐波那契数列任两项不相等,以上式子不成立。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main()
{
 LL n;
 cin >> n;
 fib[0] = fib[1] = 1;
 for (int i = 2; i <= 50; i++) fib[i] = fib[i-1]+fib[i-2];
 LL ans = 0;
 for (int i = 1; fib[i] < n; i++) {
 LL d, x, y, k;
 exgcd(fib[i-1], fib[i], d, x, y);
 LL p = fib[i]/d, q = fib[i-1]/d;
 x = x*(n/d), y = y*(n/d);
 if (x <= 0) {
 k = ceil((1.0-x)/p);
 x += k*p;
 y -= k*q;
 }
 if (x >= 0) {
 k = (x-1)/p;
 x -= k*p;
 y += k*q;
 }
 if (x <= 0 || y <= 0) continue;
 ans = ((y-1)/q+1+ans)%MOD;
 }
 cout << ans << endl;
 return 0;
}

同余

同余概述

定义 6:设 $m$ 是正整数,若 $a$ 和 $b$ 是整数,且 $m\mid (a-b)$,则称 $a$ 和 $b$ 模 $m$ 同余。

若 $a$ 和 $b$ 模 $m$ 同余,则记 $a\equiv b(mod\ m)$。

若 $m \nmid (a-b)$,则记 $a\not\equiv b (mod\ m)$,并称 $a$ 模 $m$ 不同余于 $b$。

整数 $m$ 称为同余的模。

定理 9:若 $a$ 和 $b$ 是整数,则 $a\equiv b(mod\ m)$ 当且仅当存在整数 $k$,使得 $a=b+km$。

证明:若 $a\equiv b(mod\ m)$,则 $m\mid (a-b)$,这说明存在整数 $k$,使得 $km=a-b$,所以 $a=b+km$。

反过来,若存在整数 $k$ 使得 $a=b+km$,则 $km=a-b$。于是,$m\mid (a-b)$,因而 $a\equiv b(mod\ m)$。

例:$19\equiv -2(mod\ 7)$ 和 $19=-2+3·7$。

定理 10:设 $m$ 是正整数,模 $m$ 的同余满足下面的性质:

  1. 自反性。若 $a$ 是整数,则 $a\equiv a(mod\ m)$。
  2. 对称性。若 $a$ 和 $b$ 是整数,且 $a\equiv b(mod\ m)$,则 $b\equiv a(mod\ m)$。
  3. 传递性。若 $a,b,c$ 是整数,且 $a\equiv b(mod\ m)$ 和 $b\equiv c(mod\ m)$,则 $a\equiv c(mod\ m)$。

证明

  1. 因为 $m\mid(a-a)=0$,所以 $a=a(mod\ m)$。
  2. 若 $a\equiv b(mod\ m)$,则 $m\mid(a-b)$。从而存在整数 $k$,使得 $km=a-b$。这说明 $(-k)m=b-a$,即 $m\mid (b-a)$。因此,$b\equiv a(mod\ m)$。
  3. 若 $a\equiv b(mod\ m)$,且 $b\equiv c(mod\ m)$,则有 $m\mid (a-b)$ 和 $m\mid (b-c)$。从而存在整数 $k$ 和 $l$,使得 $km=a-b,lm=b-c$。于是,$a-c=(a-b)+(b-c)=km+lm=(k+l)m$。因此,$m\mid(a-c), a\equiv c(mod\ m)$。

由定理 10 可见,整数的集合被分成 $m$ 个不同的集合,这些集合称为模 $m$ 剩余类(同余类),每个同余类中的任意两个整数都是模 $m$ 同余的。

注意,当 $m=2$ 时,正好整数分成奇、偶两类.

如果你对集合上的关系比较熟悉,那么定理 10 表明对正整数 $m$ 的模 $m$ 同余是一种等价关系,并且每一个模 $m$ 同余类即是由此种等价关系所定义的等价类。

例模 $4$ 的四个同余类是:

$$ …\equiv 8\equiv -4\equiv 0\equiv 4\equiv 8\equiv …(mod\ 4) \\ …\equiv -7\equiv -3\equiv 1\equiv 5\equiv 9\equiv …(mod\ 4) \\ …\equiv -6\equiv -2\equiv 2\equiv 6\equiv 10\equiv …(mod\ 4) \\ …\equiv -5\equiv -1\equiv 3\equiv 7\equiv 11\equiv …(mod\ 4) \\ $$

设 $m$ 是正整数,给定整数 $a$,由带余除法有 $a = bm + r$,其中 $0\le r \lt m-1$,称 $r$ 为 $a$ 的模 $m$ 最小非负剩余,是 $a$ 模 $m$ 的结果,类似地,当 $m$ 不整除 $a$ 时,称 $r$ 为 $a$ 的模 $m$ 最小正剩余。

$mod\ m$ 实际上是从整数集到集合 ${0,1,2,…,m-1}$ 的函数

定理 11:若 $a$ 与 $b$ 为整数,$m$ 为正整数,则 $a\equiv b(mod\ m)$ 当且仅当 $a \bmod m = b \bmod m$。

证明

  1. 若 $a\equiv b(mod\ m)$ 则 $a\bmod m = b \bmod m$。

由带余除法 $a=pm+ra , b=qm+rb$,因 $a\equiv b(mod\ m)$,则 $m\mid a-b$,则 $m\mid (p-q)m+(ra-rb)$,则存在整数 $x$,满足 $xm = (p-q)m+(ra-rb)$,则 $ra-rb = (x-p+q)m$,则 $ra-rb\mid m$。

因 $0\le ra\lt m, 0\le rb\lt m$, 故 $-(m-1) \le ra-rb \lt m$,故 $ra-rb$ 只能是 $0$ 才能满足 $ra-rb\mid m$。

则 $ra = rb$,则 $ra=a \bmod m, rb=b \bmod m$,则 $a \bmod m = b \bmod m$。

  1. 若 $a \bmod m = b \bmod m$ 则 $a\equiv b(mod\ m)$。

由带余除法 $a=pm+ra , b=qm+rb$,若 $a \bmod m = b \bmod m$,则 $ra=rb$。

因此 $a-b = (pm+ra)-(qm+rb)=(p-q)m + (ra-rb) = (p-q)m$。因此 $m\mid a-b$,故 $a\equiv b(mod\ m)$。

因此,每个整数都和 $0,1,…,m-1$(也就是 $a$ 被 $m$ 除所得的余数)中的一个模 $m$ 同余。因为 $0,1,…,m-1$ 中的任何两个都不是模 $m$ 同余的,所以有 $m$ 个整数使得每个整数都恰与这 $m$ 个整数中的一个同余。

定理 12:若 $a,b,c,m$ 是整数,$m\gt 0$,且 $a\equiv b(mod\ m)$,则

  1. $a+c\equiv b+c(mod\ m)$
  2. $a-c\equiv b-c(mod\ m)$
  3. $ac\equiv bc(mod\ m)$

定理 13:若 $a,b,c,m$ 是整数,$m\gt 0, d=(c,m)$,且有 $ac\equiv bc(mod\ m)$,则 $a\equiv b(mod\ m/d)$。

证明:若 $ac\equiv bc(mod\ m)$,所以存在整数 $k$ 满足 $c(a-b)=km$,两边同时除以 $d$,得到:$(c/d)(a-b)=k(m/d)$,因为 $(m/d,c/d)=1$,根据引理 2,$m/d\mid a-b$,因此 $a\equiv b(mod\ m/d)$。

下面的推论是定理 13 的特殊情形,经常用到,它使得我们能够在模 $m$ 同余式中消去与模 $m$ 互素的数。

推论 3:若 $a,b,c,m$ 是整数,$m\gt 0,(c,m)=1$,且有 $ac\equiv bc(mod\ m)$,则 $a\equiv b(mod\ m)$。

定理 14 :若 $a,b,c,m$ 是整数,$m\gt 0,a\equiv b(mod\ m)$,且 $c\equiv d(mod\ m)$,则:

  1. $a+c\equiv b+d(mod\ m)$
  2. $a-c\equiv b-d(mod\ m)$
  3. $ac\equiv bd(mod\ m)$

证明

因为 $a\equiv b(mod\ m)$ 且 $c\equiv d(mod\ m)$,我们有 $m\mid (a-b)$ 与 $m\mid (c-d)$。因此,存在整数 $k$ 与 $l$ 使得 $km=a-b,lm=c-d$。

为证 1,注意到 $(a+c)-(b+d)=(a-b)+(c-d)=km+lm=(k+l)m$.因此 $m\mid (a+c)-(b+d)$,即 $a+c\equiv b+d(mod\ m)$。

为证 2,注意到 $(a-c)-(b-d)=(a-b)-(c-d)=km-lm=(k-1)m$,因此 $m\mid (a-c)-(b-d)$,即 $a-c\equiv b-d(mod\ m)$.

为证 3,注意到 $ac-bd=ac-bd+bc-bd=c(a-b)+b(c-d)=ckm+blm=m(ck+ bl)$。因此,$m\mid ac-bd$,即 $ac\equiv bd(mod\ m)$。

定义 7:一个模 $m$ 完全剩余系是一个整数的集合,使得每个整数恰和此集合中的一个元素模 $m$ 同余。

例如:整数 $0,1,…,m-1$ 的集合是模 $m$ 完全剩余系,称为模 $m$ 最小非负剩余的集合。

下面的引理帮助我们判定一个 $m$ 元集合是否为模 $m$ 的完全剩余系.

引理 3:$m$ 个模 $m$ 不同余的整数的集合构成一个模 $m$ 的完全剩余系。

证明

假设 $m$ 个模 $m$ 不同余的整数集合不是模 $m$ 完全剩余系,这说明,至少有一个整数 $a$ 不同余于此集合中的任一整数。

所以此集合中的整数都模 $m$ 不同余于 $a$ 被 $m$ 除所得的余数,从而,整数被 $m$ 除得的不同剩余至多有 $m-1$ 个。

由鸽笼原理,此集合中至少有两个整数有相同的模 $m$ 剩余。

这不可能,因为这些整数均模 $m$ 不同余,因此,$m$ 个模 $m$ 不同余的整数的集合构成一个模 $m$ 的完全剩余系。

定理 15:若 $r_1,r_2,…,r_m$ 是一个模 $m$ 的完全剩余系,且正整数 $a$ 满足 $(a,m)=1$,则对任何整数 $b$,$ar_1+b, ar_2+b,…,ar_m+b$ 都是模 $m$ 的完全剩余系。

证明:首先来证整数 $ar_1+b, ar_2+b,…,ar_m+b$ 中的任何两个都模 $m$ 不同余。

反证,若存在 $1\le j,k\le m$ 且 $j\ne k$ 且 $ar_j+b \equiv ar_k+b(mod\ m)$,则由定理 12.2 可知:$ar_j \equiv ar_k(mod\ m)$。因为 $(a,m)=1$,推论 3 表明 $r_j\equiv r_k(mod\ m)$,这与 $r_1,r_2,…,r_m$ 是一个模 $m$ 的完全剩余系相矛盾。

故 $ar_1+b, ar_2+b,…,ar_m+b$ 是 $m$ 个模 $m$​ 不同余的整数,由引理 3,命题得证。

定理 16:若 $a,b,k,m$ 是整数,$k\gt 0,m\gt 0$,且 $a\equiv b(mod\ m)$,则 $a^k\equiv b^k(mod\ m)$。

证明:因为 $a\equiv b(mod\ m)$,所以 $m\mid a-b$。

因为 $a^k-b^k =(a-b)(a^{k-1}+a^{k-2}b+…ab^{k-2}+b^{k-1})$ (可以参考资料:详聊如何理解a^n-b^n因式分解

所以 $a-b \mid a^k-b^k$,根据 定理 1,$m \mid a^k-b^k$,即 $a^k\equiv b^k(mod\ m)$。

线性同余方程

设 $x$ 是未知整数,形如 $ax\equiv b(mod\ m)$ 的同余式称为一元线性同余方程。

首先注意到,若 $x=x_0$ 是同余方程 $ax\equiv b(mod\ m)$ 的一个解,且 $x_1\equiv x_0(mod\ m)$,则 $ax_1\equiv ax_0 \equiv b(mod\ m)$,所以 $x_1$ 也是一个解。

因此,若一个模 $m$ 同余类的某个元素是解,则此同余类的所有元素都是解。

于是,我们会问模 $m$ 的 $m$ 个同余类中有多少个是给出方程的解,这相当于问方程有多少个模 $m$ 不同余的解。

定理 17:设 $a,b,m$ 是整数,$m\gt 0,(a,m)=d$。

若 $d\nmid b$,则 $ax\equiv b(mod\ m)$ 无解。

若 $d\mid b$,则 $ax\equiv b(mod\ m)$ 恰有 $d$ 个模 $m$ 不同余的解。

证明:根据定理 9,线性同余方程 $ax\equiv b(mod\ m)$ 可以写成二元线性不定方程 $ax+my=b$。其中 $x, y$ 是未知数。整数 $x$ 是 $ax\equiv b(mod\ m)$ 的解当且仅当存在整数 $y$ 使得 $ax+my=b$。

由定理 8 可知,若 $d\nmid b$,则无解,而 $d\mid b$ 时,$ax-my=b$ 有无穷多解:$x = x_0 + (m/d)t, y = y - (a/d)t$,

其中 $x=x_0$ 和 $y=y_0$ 是方程的特解,上述 $x$ 的值 $x=x_0+(m/d)t$ 是线性同余方程的解,有无穷多这样的解。

为确定有多少不同余的解,我们来找两个解 $x_1=x_0+(m/d)t1$ 和 $x_2=x_0+(m/d)t2$ 模 $m$ 同余的条件,若这两个解同余,则 $(x0+(m/d)t1)\equiv (x0+(m/d)t2) (mod\ m)$。

根据 定理 12,两边减去 $x_0$,有 $(m/d)t1\equiv (m/d)t2(mod\ m)$。

因为 $(m/d)\mid m$,所以 $(m,m/d)=m/d$,再由 定理 13 得,$t1\equiv t2 (mod\ d)$。

这表明不同余的解的一个完全集合可以通过取 $x=x_0+(m/d)t$ 得到,其中 $t$ 取遍模 $d$ 的完全剩余系,一个这样的集合可由 $x=x_0+(m/d)t$ 给出,其中 $t=0,1,2,…,d-1$。

推论 4:若 $a$ 和 $m\gt 0$ 互素,且 $b$ 是整数,则线性同余方程 $ax\equiv b(mod\ m)$ 有模 $m$ 的唯一解。

证明

因为 $(a,m)=1$,所以 $(a,m)\mid b$。因此,由 定理 17,线性同余方程 $ax\equiv b(mod\ m)$ 恰有 $(a,m)=1$ 个模 $m$ 不同余的解。

:为求出 $9x\equiv 12(mod\ 15)$ 的所有解,首先注意到因为 $(9,15)=3$ 且 $3\mid 12$,所以恰有三个不同余的解,我们可以通过先找到一个特解,再加上 $15/3=5$ 的适当倍数来求得所有的解。

为求特解,我们考虑二元线性不定方程 $9x-15y=12$。由扩展欧几里得算法得到:$9x-15y=12$ 的一个特解是 $x_0=8$ 和 $y_0=4$。

由定理 17 的证明可知,三个不同余的解由 $x= x_0\equiv 8(mod\ 15),x= x_0+5\equiv 13(mod\ 15)$ 和 $x= x_0+5*2\equiv 18\equiv 3(mod\ 15)$ 给出。

习题

Luogu-P1516 青蛙的约会 Orz 题解 P1516 【青蛙的约会】 - FlashHu

如果两蛙相遇,那么他们的初始坐标差 $x-y$ 和跳的距离 $(n-m)t$ 之差应该模纬度线总长 $l$ 同余,$(n-m)t\equiv x-y(mod\ l)$。转化成不定方程的形式:$(n-m)t+kl=x-y$,并求最小正整数解。设 $a=n-m,b=l,c=x-y$,可以写成 $ax+by = c$,通过 exgcd 可以求出 x 的一个特解。

细节问题,因为 gcd 只对非负整数有意义,如果 $a\lt 0$ 时等式两边要同时取负,$a,c$ 变成相反数(相当于把两个蛙交换了一下),$b$ 是正数所以不能变。

这里求最小正整数解时用了模的方法来处理,值得细品 (x0%p+p)%p

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int main()
{
 cin >> x >> y >> m >> n >> l;
 LL a = n-m, b = l, c = x-y, d, x0, y0;
 if (a < 0) {
 a = -a;
 c = -c;
 }
 exgcd(a, b, d, x0, y0);
 if (c % d == 0) {
 x0 *= c/d;
 y0 *= c/d;
 LL p = b/d;
 cout << (x0%p+p)%p << endl;
 } else {
 cout << "Impossible\n";
 }
 return 0;
}

POJ-2115 – C Looooops

可将题意转化为 $A+Ct\equiv B(mod\ 2^K)$,把它转化成方程:$A+Ct-B=2^Kz$,即 $Ct+2^Kz=B-A$,用 exgcd 解这个方程并求出 $t$ 的最小正整数解即为答案。

注意 1LL<<K,不开 long long 差点怀疑自己做错了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
while (cin >> A >> B >> C >> K && !(A==0&&B==0&&C==0&&K==0)) {
 K = 1ll<<K;
 LL a = C, b = K, d, x, y;
 exgcd(a, b, d, x, y);
 if (((B-A+K)%K)%d) {
 cout <<"FOREVER\n";
 } else {
 x *= ((B-A+K)%K)/d;
 LL p = b/d;
 x = (x%p+p)%p;
 cout << x << endl;
 }
}

模的逆

现在考虑特殊形式的同余方程 $ax\equiv 1(mod\ m)$。

由 定理 17,此方程有解当且仅当 $(a,m)=1$,于是其所有的解都模 $m$ 同余。

定义 8:给定整数 $a$,且满足 $(a,m)=1$,称 $ax\equiv 1(mod\ m)$ 的一个解为 $a$ 模 $m$ 的逆。

:因为 $7x\equiv 1(mod\ 31)$ 的解满足 $x\equiv 9(mod\ 31)$,所以 $9$ 和所有与 $9$ 模 $31$ 同余的整数都是 $7$ 模 $31$ 的逆。类似地,因为 $9·7\equiv 1(mod\ 31)$,所以 $7$ 是 $9$ 模 $31$ 的逆。

当我们有 $a$ 模 $m$ 的一个逆时,可以用它来解形如 $ax\equiv b(mod\ m)$ 的任何同余方程。

为看清这一点,令 $\bar{a}$ 是 $a$ 的模 $m$ 的一个逆,所以 $a\bar{a}\equiv 1(mod\ m)$。

于是,若 $ax\equiv b(mod\ m)$,则根据 定理 12 将同余方程两边同时乘以 $\bar{a}$,得到 $\bar{a}(ax)\equiv \bar{a}b(mod\ m)$,所以 $x=\bar{a}b(mod\ m)$。

为求出 $7x\equiv 22(mod\ 31)$ 的所有解,我们在此方程两边同时乘以 $9$(这是 $7$ 模 $31$ 的一个逆),得 $9·7x\equiv 9·22(mod\ 31)$。因此,$x\equiv 198=12(mod\ 31)$。

求逆的三种算法:

  1. 扩展欧几里得算法解同余方程,求单个逆。$ax\equiv 1(mod\ m)$ 等价于求二元线性不定方程的解:$ax+my=1$,其中 $(a,m)=1$ 是有逆的前提。只需要用扩展欧几里得算法求即可。

  2. 费马小定理,求单个逆

定理 18(费马小定理):设 $p$ 是一个素数,$a$ 是一个正整数且 $p\nmid a$,则 $a^{p-1}\equiv 1(mod\ p)$。

证明:考虑 $p-1$ 个整数 $a,2a,…,(p-1)a$,它们都不能被 $p$ 整除。因为若 $p \mid ja$, 那么因 $p \nmid a$,则由 引理 2 知 $p \mid j$,但 $1\le j\le p-1$,故这是不可能的。

现证明 $a,2a,…,(p-1)a$ 中任何两个整数模 $p$ 不同余。

为了证明这一点,设 $ja\equiv ka(mod\ p)$,其中 $1\le j\lt k\le p-1$。

那么根据 推论 3,因 $(a,p)=1$,故 $j\equiv k(mod\ p)$,但这也是不可能的,因为 $j$ 和 $k$ 都是小于 $p-1$ 的正整数.

因为整数 $a,2a,…,(p-1)a$ 是 $p-1$ 个满足模 $p$ 均不同余于 $0$ 且任何两个都互不同余的整数组成的集合中的元素,故由 引理 3 可知 $a,2a,…,(p-1)a$ 模 $p$ 的最小正剩按照一定的顺序必定是整数 $1,2,…,p-1$。

由同余性,整数 $a,2a,…,(p-1)a$ 的乘积模 $p$ 同余于前 $p-1$ 个正整数的乘积,即:

$a·2a·3a···(p-1)a \equiv 1·2·3···(p-1) (mod\ p)$

因此 $a^{p-1}(p-1)! \equiv (p-1)! (mod\ p)$, 因 $(p,(p-1)!)=1$,根据 推论 3,消去 $(p-1)!$,得到 $a^{p-1}\equiv 1(mod\ p)$,证毕。

利用费马小定理,$a^{p-1}\equiv 1(mod\ p)$,则 $a·a^{p-2} \equiv 1(mod\ p)$,即 $a^{p-2}$ 是 $a$ 模 $p$ 的一个逆。

注意前提:$p$ 是一个素数,$a$ 是一个正整数且 $p \nmid a$。

通常可以用快速幂求 $a^{p-2}$。

  1. 若 $p$ 是素数,$n\lt p$,线性递推求 $1$ 至 $n$ 在模 $p$ 意义下的乘法逆元

首先,$i=1$ 的逆是 $1$。下面求 $i\gt 1$ 的逆,用递推法。

  1. 用带余除法 $p = k·i + r$,其中 $0\le r \lt i$,故 $k·i + r \equiv 0 (mod\ p)$
  2. 在等式两边乘 $i^{-1}r^{-1}$,即 $(k·i + r)·i^{-1}r^{-1} \equiv 0 (mod\ p)$,即 $kr^{-1} + i^{-1} \equiv 0 (mod\ p)$
  3. 移项得 $i^{-1}\equiv -kr^{-1} (mod\ p)$,即 $i^{-1}\equiv (-p/i)r^{-1} (mod\ p)$。若要避免负数,因 $pr^{-1} \equiv 0 (mod\ p)$,由定理 12 得,$pr^{-1} + (-p/i)r^{-1} \equiv (-p/i)r^{-1} (mod\ p)$ ,即 $(p-p/i)r^{-1} \equiv (-p/i)r^{-1} (mod\ p) $。

则 $i^{-1}\equiv (p-p/i)r^{-1} (mod\ p)$。

代码:

1
2
3
4
void inverse(LL n, LL p) {
 inv[1] = 1;
 for (int i = 2; i <= n; i++) inv[i] = (p-p/i)*inv[p%i]%p;
}

逆与除法取模

逆的一个重要应用是求除法的模。求 $(a/b) \bmod m$,即 $a$ 除以 $b$,然后对 $m$ 取模。

这里 $a$ 和 $b$ 都是很大的数,如 $a=n!$,容易溢出,导致取模出错。

用逆可以避免除法计算,设 $b$ 的逆元是 $b^{-1}$,有 $(a/b) \bmod m = ((a/b) \bmod m) ((bb^{-1}) \bmod m) = (a/b×bb^{-1}) \bmod m = (ab^{-1}) \bmod m$

经过上述推导,除法的模运算转换为乘法模运算,即

$(a/b) \bmod m = (ab^{-1}) \bmod m = (a \bmod m) (b^{-1} \bmod m) \bmod m$

习题

HDU-1576 A/B 因为 $\gcd(B,9973)=1$,可以用 exgcd 求逆元。

1
2
3
4
5
6
7
8
9
while (t--) {
 LL n, B;
 cin >> n >> B;
 LL a, b, d, x, y;
 exgcd(B, 9973, d, x, y);
 LL p = 9973;
 x = (x%p+p)%p;
 cout << n*x%9973 << endl;
}

更多资料

ABC209F Deforestation

2023年4月28日 08:00

ABC209F Deforestation

题意:给出 $n$ 棵树的高度,砍第 $i$ 棵树的花费是 $h_i+h_{i-1}+h_{i+1}$,求有多少种方案能使得砍完所有树的总代价最小。


砍一棵树的代价只与相邻的树高度有关。下面研究砍 $h_i$ 与 $h_{i+1}$ 的先后顺序对答案的影响。

  1. 先砍 $h_i$ 后砍 $h_{i+1}$:$h_i+h_{i-1}+h_{i+1}+h_{i+1}+h_{i+2}$
  2. 先砍 $h_{i+1}$ 后砍 $h_i$:$h_{i+1}+h_i+h_{i+2}+h_i+h_{i-1}$

作差后得到:$h_{i+1}-h_i$。当 $h_{i+1}>h_i$ 时,应该先砍 $h_{i+1}$。当 $h_{i+1}<h_i$ 时,应该先砍 $h_i$。因此,对于相邻的两棵树,先砍高的那棵最优


插入 DP(insertion DP):先考虑排好前 $i-1$ 个数,再往中间插入第 $i$ 个数。

令 $\mathit{f}_{i,j}$ 表示排好了前 $i$ 棵树的砍树次序,且第 $i$ 棵树排在第 $j$ 位,得到最小代价的方案数。

当 $h_{i+1} > h_i$ 时,应先砍 $i+1$,那么 $\mathit{f}{i+1,j} = \sum{k=j}^{i}\mathit{f}_{i,k}$

当 $h_i > h_{i+1}$ 时,应先砍 $i$,那么 $\mathit{f}{i+1,j} = \sum{k=1}^{j-1}\mathit{f}_{i,k}$

当 $h_i = h_{i+1}$ 时,砍哪棵都可以,$\mathit{f}{i+1,j} = \sum{k=1}^i\mathit{f}_{i,k}$

可以用前缀和优化 DP,$O(n^2)$。

弱智错误!:减法忘记加 MOD。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL MOD = 1e9+7;
LL n, h[4005];
LL dp[4005][4005], sum[4005][4005];
int main()
{
 cin >> n;
 for (int i = 1; i <= n; i++) cin >> h[i];
 dp[1][1] = sum[1][1] = 1;
 for (int i = 2; i <= n; i++) {
 for (int j = 1; j <= i; j++) {
 if (h[i] > h[i-1]) {
 dp[i][j] = (sum[i-1][i-1]-sum[i-1][j-1]+MOD)%MOD;
 } else if (h[i] < h[i-1]) {
 dp[i][j] = (sum[i-1][j-1])%MOD;
 } else {
 dp[i][j] = (sum[i-1][i-1])%MOD;
 }
 sum[i][j] = (sum[i][j-1]+dp[i][j])%MOD;
 }
 }
 LL ans = 0;
 for (int i = 1; i <= n; i++) ans = (ans+dp[n][i])%MOD;
 cout << ans << endl;
 return 0;
}

Thanks to ABC 209 F - Deforestation - hzy0227

CDQ 分治笔记

2023年4月28日 08:00

基本思想

CDQ 分治的基本思想十分简单。如下:

  1. 我们要解决一系列问题,这些问题一般包含修改和查询操作,可以把这些问题排成一个序列,用一个区间 $[L,R]$ 表示。
  2. 分。递归处理左边区间 $[L,M]$ 和右边区间 $[M+1,R]$ 的问题。
  3. 治。合并两个子问题,同时考虑到 $[L,M]$ 内的修改对 $[M+1,R]$ 内的查询产生的影响。即,用左边的子问题帮助解决右边的子问题。

这就是 CDQ 分治的基本思想。和普通分治不同的地方在于,普通分治在合并两个子问题的过程中,$[L,M]$ 内的问题不会对 $[M+1,R]$ 内的问题产生影响。

前置知识:二维偏序

给定 $N$ 个有序对 $(a,b)$,求对于每个 $(a,b)$,满足 $a2<a$ 且 $b2<b$ 的有序对 $(a2,b2)$ 有多少个。

可以将归并排序求逆序对的思路套用过来,这题实际上就是求顺序对。首先根据 $a$ 的大小排序,然后归并排序 $b$,这样就可以忽略 $a$ 元素的影响,因为左边区间的元素的 $a$ 一定小于右边元素的 $a$。归并排序时,每次从右边区间的有序序列取一个元素,然后求左边区间多少个元素比它小即可。

更浅显的解法是,用树状数组代替 CDQ 分治。这里就不赘述。

放个求逆序对的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void mergesort(int l, int r) {
 if (l >= r) return ;
 int mid = (l+r)/2;
 mergesort(l, mid);
 mergesort(mid+1, r);
 int lp = l, rp = mid+1;
 int i = l;
 while (lp <= mid && rp <= r) {
 if (a[lp] > a[rp]) {
 ans += mid-lp+1;
 b[i++] = a[rp++];
 } else {
 b[i++] = a[lp++];
 }
 }
 while (lp <= mid) b[i++] = a[lp++];
 while (rp <= r) b[i++] = a[rp++];
 for (int i = l; i <= r; i++)
 a[i] = b[i];
}

例题

一:三维偏序

Luogu-P3810 【模板】三维偏序(陌上花开)

给定 $N$ 个有序三元组 $(a,b,c)$,求对于每个三元组 $(a,b,c)$,有多少个三元组$(a2,b2,c2)$ 满足$a2<a$ 且 $b2<b$ 且 $c2<c$。

同样地,我们要保证前两维的顺序,才可以计算出答案。

先按 $a$ 进行排序,然后在归并过程中按 $b$ 排序,但是此时不能像求逆序对一样利用下标轻松地统计个数了。

怎么办呢?我们可以维护 $c$ 的树状数组。如果 $b2 \le b$,那么 $c2$ 就可以对以后的 $b$ 产生贡献了,把 $c2$ 加入树状数组。统计时查询树状数组中 $c$ 的前缀和即可。

时间复杂度:$T(n)=O(n\log k)+T(\frac 2 n)=O(n\log n\log k)$

有一些细节问题:

  • 每次处理完后,树状数组清零不要用 memset,否则会超时。要一个一个减去。
  • 可能有完全相同的元素。本来它们相互之间都有贡献,可是 CDQ 的过程中只有左边的能贡献右边的。所以需要进行去重处理。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <bits/stdc++.h>
using namespace std;
struct node {
 int a, b, c;
 int cnt, ans;
} a[100005], b[100005];
bool cmp1(node a, node b) {
 if (a.a != b.a) return a.a < b.a;
 if (a.b != b.b) return a.b < b.b;
 return a.c < b.c;
}
bool cmp2(node a, node b) {
 if (a.b != b.b) return a.b < b.b;
 return a.c < b.c;
}
int n, k, m;
int tree[200005], ans[200005];
int lowbit(int x) { return x&(-x); }
int query(int x) {
 int ret = 0;
 while (x) {
 ret += tree[x];
 x -= lowbit(x);
 }
 return ret;
}
void add(int x, int y) {
 while (x <= k) {
 tree[x] += y;
 x += lowbit(x);
 }
}
void cdq(int l, int r) {
 if (l == r) return;
 int mid = (l+r)>>1;
 cdq(l, mid);
 cdq(mid+1, r);
 sort(b+l, b+mid+1, cmp2);
 sort(b+mid+1, b+r+1, cmp2);
 int p = l, q = mid+1;
 while (q <= r) {
 while (p <= mid && b[p].b <= b[q].b) {
 add(b[p].c, b[p].cnt);
 p++;
 }
 b[q].ans += query(b[q].c);
 q++;
 }
 for (int i = l; i < p; i++) add(b[i].c, -b[i].cnt);// 拒绝 memset!
}
int main()
{
 cin >> n >> k;
 for (int i = 1; i <= n; i++) cin >> a[i].a >> a[i].b >> a[i].c;
 sort(a+1, a+1+n, cmp1);
 for (int i = 1; i <= n; i++) {
 int j = i;
 while (a[j+1].a == a[i].a && a[j+1].b == a[i].b && a[j+1].c == a[i].c) j++;
 b[++m] = a[i];
 b[m].cnt = j-i+1;
 i = j;
 }
 cdq(1, m);
 for (int i = 1; i <= m; i++)
 ans[b[i].ans+b[i].cnt-1] += b[i].cnt;
 for (int i = 0; i < n; i++) cout << ans[i] << endl;
 return 0;
}

二:天使玩偶/SJY 摆棋子

Luogu-P4169 Violet 天使玩偶/SJY 摆棋子

要算的 $dist(A,B)=|A_x-B_x|+|A_y-B_y|$ 有绝对值,为了简化问题,可以把原来的询问分成四个,分别计算在 $(x,y)$ 的左下、左上、右下、右上方向上距离最近的点有多远,再去最小值即为答案。

以左下方向为例,化简后得到 $(x+y)-\max(x_i+y_i)$,若扫描到一个点 $(x_i, y_i)$,则在树状数组中把 $y_i$ 位置上的值与 $x_i+y_i$ 取最大值,扫描到询问时,则求 $[0, y_i]$ 上的最大值 $val$,该询问(左下方向)的答案就是 $x+y-val$。简言之,套 CDQ,按照 $x$ 进行排序,然后树状数组处理 $y$。

对于另外三个方向,可以进行翻转后转换成左下方向。记所有点最大的 $x$ 或 $y$ 为 $maxxy$,例如将右上方向翻到左下方向时,坐标变成 $(maxxy-x, maxxy-y)$。为了避免出现 $0$ 使树状数组爆炸,可以将所有 $x$ 和 $y$ 值 $+1$,$maxxy$ 也要 $+1$。

要在 CDQ 过程中对 $x$ 进行归并排序,sort 太慢。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <bits/stdc++.h>
using namespace std;
const int N = 3000005;
struct node {
 int type, x, y, id, ans;
} a[N], b[N], tmp[N];
int n, m, maxxy;
int tree[N];
int lowbit(int x) { return x&(-x); }
int query(int x) {
 int ret = 0;
 while (x) {
 ret = max(ret, tree[x]);
 x -= lowbit(x);
 }
 return ret ? ret : -1e9;
}
void upd(int x, int y) {
 while (x <= maxxy) {
 tree[x] = max(y, tree[x]);
 x += lowbit(x);
 }
}
void clear(int x) {
 while (x <= maxxy) {
 tree[x] = 0;
 x += lowbit(x);
 }
}
void cdq(int l, int r) {
 if (l == r) return;
 int mid = (l+r)>>1;
 cdq(l, mid);
 cdq(mid+1, r);
 int p = l, q = mid+1, t = l;
 while (q <= r) {
 while (p <= mid && b[p].x <= b[q].x) {
 if (b[p].type == 1)
 upd(b[p].y, b[p].x+b[p].y);
 tmp[t++] = b[p++];
 }
 if (b[q].type == 2)
 a[b[q].id].ans = min(a[b[q].id].ans, b[q].x+b[q].y-query(b[q].y));
 tmp[t++] = b[q++];
 }
 for (int i = l; i < p; i++) if (b[i].type == 1) clear(b[i].y);
 while (p <= mid) tmp[t++] = b[p++];
 for (int i = l; i <= r; i++) b[i] = tmp[i];
}
int main()
{
 cin >> n >> m;
 for (int i = 1; i <= n; i++) {
 cin >> a[i].x >> a[i].y;
 a[i].x++;
 a[i].y++;
 a[i].type = 1;
 a[i].id = i;
 maxxy = max(maxxy, max(a[i].x, a[i].y));
 }
 for (int i = n+1; i <= n+m; i++) {
 cin >> a[i].type >> a[i].x >> a[i].y;
 a[i].x++;
 a[i].y++;
 a[i].id = i;
 a[i].ans = 1e9;
 maxxy = max(maxxy, max(a[i].x, a[i].y));
 }
 maxxy++;

 for (int i = 1; i <= n+m; i++) b[i] = a[i];
 cdq(1, n+m);
 for (int i = 1; i <= n+m; i++) {
 b[i] = a[i];
 b[i].x = maxxy-a[i].x;
 b[i].y = maxxy-a[i].y;
 }
 cdq(1, n+m);
 for (int i = 1; i <= n+m; i++) {
 b[i] = a[i];
 b[i].x = maxxy-a[i].x;
 }
 cdq(1, n+m);
 for (int i = 1; i <= n+m; i++) {
 b[i] = a[i];
 b[i].y = maxxy-a[i].y;
 }
 cdq(1, n+m);

 for (int i = n+1; i <= n+m; i++)
 if (a[i].type == 2) cout << a[i].ans << endl;

 return 0;
}

三:动态逆序对

Luogu-P3157 [CQOI2011]动态逆序对

可以求出初始逆序对,然后每次求出降低的逆序对个数即可。

对于每一个被删的数,它对逆序对个数的影响可分为两类:

  • 在它前面,权值比它大,删除时间比它晚的点个数
  • 在它后面,权值比它小,删除时间比它晚的点个数

这样就可以转化为三维偏序了。

参考资料

ABC133F Colorful Tree

2023年4月14日 08:00

F - Colorful Tree

题意

有一个 $N$ 个节点的树,每条边有颜色、边权。

您需要处理 $Q$ 个询问,每个询问给出 $x_i,y_i,u_i,v_i$,您需要求出假定所有颜色为 $x_i$ 的边边权全部变成 $y_i$ 后,$u_i$ 和 $v_i$ 之间的距离。询问之间互相独立

分析

DFS 序的思想套上主席树,root[i] 的权值线段树存从根到 $i$ 结点的每种颜色的边数($cnt$),以及该颜色的长度和($sum$)。顺便记录从根到 $i$ 结点的距离。利用差分,$dis(i, j) = dis(root, i)+dis(root, j)-2dis(root, LCA(i, j)$。然后 $i, j$ 的路径中该颜色的长度和也用同样的方法求出。那么答案就是 $dis(i, j) - \text{该颜色的长度和} + \text{该颜色的边数}*y$。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <bits/stdc++.h>
using namespace std;
const int N = 100005, LOGN = 20;
struct edge {
 int to, col, len;
};
vector<edge> g[N];
int n, bn, m, root[N], depth[N], f[N][LOGN], dis[N], sum[25*N], lson[25*N], rson[25*N], tot[25*N], cnt;
void modify(int &rt, int prt, int pos, int val, int l, int r) {
 rt = ++cnt;
 sum[rt] = sum[prt]+val;
 tot[rt] = tot[prt]+1;
 if (l == r) { return; }
 lson[rt] = lson[prt];
 rson[rt] = rson[prt];
 int mid = (l+r)>>1;
 if (pos <= mid)
 modify(lson[rt], lson[prt], pos, val, l, mid);
 else
 modify(rson[rt], rson[prt], pos, val, mid+1, r);
}
int query(int rt, int pos, int y, int l, int r) {
 if (l == r) return y*tot[rt]-sum[rt];
 int mid = (l+r)>>1;
 if (pos > mid) return query(rson[rt], pos, y, mid+1, r);
 else return query(lson[rt], pos, y, l, mid);
}
void dfs(int u, int fa) {
 f[u][0] = fa;
 depth[u] = depth[fa]+1;
 for (int i = 1; i < LOGN; i++)
 f[u][i] = f[f[u][i-1]][i-1];
 for (int i = 0; i < g[u].size(); i++){
 int v = g[u][i].to;
 if (v == fa) continue;
 dis[v] = dis[u]+g[u][i].len;
 modify(root[v], root[u], g[u][i].col, g[u][i].len, 1, n);
 dfs(v, u);
 }
}
int LCA(int u, int v) {
 if (depth[u] > depth[v]) swap(u, v);
 for (int i = LOGN-1; i >= 0; i--)
 if (depth[f[v][i]] >= depth[u])
 v = f[v][i];
 for (int i = LOGN-1; i >= 0; i--) {
 int s = f[u][i], t = f[v][i];
 if (s != t) {
 u = s;
 v = t;
 }
 }
 if (u != v) return f[u][0];
 return u;
}
int main()
{
 cin >> n >> m;
 for (int i = 1; i < n; i++) {
 int a, b, c, d;
 cin >> a >> b >> c >> d;
 g[a].push_back({b, c, d});
 g[b].push_back({a, c, d});
 }
 root[1] = ++cnt;
 dfs(1, 1);
 while (m--) {
 int a, b, c, d;
 cin >> a >> b >> c >> d;
 int lc = LCA(c, d);
 int ans = dis[c]+dis[d]-2*dis[lc];
 ans += query(root[c], a, b, 1, n)+query(root[d], a, b, 1, n)-2*query(root[lc], a, b, 1, n);
 cout << ans << endl;
 }
 return 0;
}

树链剖分笔记

2023年2月25日 08:00

树链剖分(本文仅介绍 重链剖分(Heavy-Light Decomposition))的用途:

  • 更新树上两点之间的路径上的所有点的值
  • 求树上两点之间的路径上的最大值、最小值、和(或任意满足结合律的运算)

思想

树链剖分即把整棵树剖分成若干条链,然后用线段树等数据结构来维护链上的信息。重链剖分可以将树上的任何一条路径划分成不超过 $O(\log n)$ 条连续的链。

定义:

  • 重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。
  • 轻子节点 表示剩余的所有子结点。
  • 从这个结点到重子节点的边为 重边
  • 到其他轻子节点的边为 轻边
  • 若干条首尾衔接的重边构成 重链

把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。

性质

  • 树上每个节点都属于且仅属于一条重链

  • 重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的)。

  • 所有的重链将整棵树 完全剖分

  • 在剖分时 重边优先遍历,最后树的 DFN 序上,重链内的 DFN 序是连续的。按 DFN 排序后的序列即为剖分后的链。

  • 一颗子树内的 DFN 序是连续的。

  • 可以发现,当我们向下经过一条 轻边 时,所在子树的大小至少会除以二。

  • 因此,对于树上的任意一条路径,把它拆分成从 $lca$ 分别向两边往下走,分别最多走 $O(\log n)$ 次,因此,树上的每条路径都可以被拆分成不超过 $O(\log n)$ 条重链。

实现

模板 RECORD

定义:

  • $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 求出以上的所有值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void dfs1(int u, int f) {
  fa[u] = f;
  dep[u] = dep[f] + 1;
  siz[u] = 1;
  for (int i = 0; i < g[u].size(); i++) {
    int v = g[u][i];
    if (v == f) continue;
    dfs1(v, u);
    if (son[u] == 0 || siz[v] > siz[son[u]]) son[u] = v;
    siz[u] += siz[v];
  }
}

void dfs2(int u, int topp) {
  top[u] = topp;
  dfn[u] = ++cnt;
  rnk[dfn[u]] = u;
  if (son[u] != 0) dfs2(son[u], topp); // 小心!要判断该节点有无重儿子
  for (int i = 0; i < g[u].size(); i++) {
    int v = g[u][i];
    if (v == fa[u] || v == son[u]) continue;
    dfs2(v, v);
  }
}

然后用 DFN 序来代表每一个点,套上普通的线段树:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void build(int u, int l, int r) {
  if (l == r) {
    tree[u].maxx = tree[u].sum = w[rnk[l]];
    return;
  }
  build(u << 1, l, mid);
  build(u << 1 | 1, mid + 1, r);
  pushup(u);
}
// ... 修改查询什么的的就不贴上来了

处理路径信息

处理两个点 $u$,$v$ 之间的信息,重复以下步骤:

  1. 如果 $u$,$v$ 不在同一条链上,设 $u$ 所在链链顶深度较大,则查出 $u$ 的链顶到 $u$ 的信息(依据同一条重链上 DFN 序是连续的),然后将 $u$ 跳到 $u$ 所在链链顶的父亲
  2. 如果 $u$,$v$ 在同一条链上,直接查询。结束。
1
2
3
4
5
6
7
8
9
int ans = -INF;
while (top[x] != top[y]) {
 if (dep[top[x]] < dep[top[y]]) swap(x, y);
 ans = max(ans, queryMax(1, 1, cnt, dfn[top[x]], dfn[x]));
 x = fa[top[x]];
}
if (dep[x] > dep[y]) swap(x, y);
ans = max(ans, queryMax(1, 1, cnt, dfn[x], dfn[y]));
cout << ans << endl;

运用同样的方法,可以求出两个点的 LCA。

处理子树信息

根据一颗子树内的 DFN 序是连续的,我们可以记录所在子树连续区间末端的结点,就可以把子树转化为连续的一段区间。

参考资料

🙇‍

CSP-J/S2022 题解与反思

2022年11月5日 08:00

学校 OI 停(tui yi)了,周末有空的时候补补。

J

T1 乘方

Luogu-P8813 [CSP-J 2022] 乘方

如果 $a^b$ 的值不超过 ${10}^9$,则输出 $a^b$ 的值,否则输出 -1。数据范围:$1 \le a, b \le {10}^9$。

$2^{30}=1073741824 > 10^9$,所以循环最多 29 次就能判断是否超过 $10^9$。注意 $1$ 的任何次幂都是 $1$,不能进行循环,特判一下即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <bits/stdc++.h>
using namespace std;
const long long MAXX = 1e9;
int main()
{
 long long a, b;
 cin >> a >> b;
 if (a == 1) {
 cout << 1 << endl;
 } else {
 long long s = 1;
 bool flag = 0;
 for (long long i = 1; i <= b && s*a <= MAXX; i++) {
 s *= a;
 if (i == b) {
 flag = 1;
 }
 }
 if (!flag) {
 cout << -1 << endl;
 } else {
 cout << s << endl;
 }
 }
 return 0;
}

T2 解密

Luogu-P8814 [CSP-J 2022] 解密

给定一个正整数 $k$,有 $k$ 次询问,每次给定三个正整数 $n_i, e_i, d_i$,求两个正整数 $p_i, q_i$,使 $n_i = p_i \times q_i$、$e_i \times d_i = (p_i - 1)(q_i - 1) + 1$。

$1 \leq k \leq {10}^5$,对于任意的 $1 \leq i \leq k$,$1 \leq n_i \leq {10}^{18}$,$1 \leq e_i \times d_i \leq {10}^{18}$ ,$1 \leq m \leq {10}^9$。

初三学生表示毫无压力,推式子然后解一元二次方程即可。

如果我是在初二考的这道题说不定会直接懵逼了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
template<typename T>
T read() {
 T s = 0, f = 1;
 char c = getchar();
 while (!isdigit(c)) {
 if (c == '-') f = -1;
 c = getchar();
 }
 while (isdigit(c)) {
 s = s*10+c-'0';
 c = getchar();
 }
 return s*f;
}
int k;
LL n, e, d;
int main()
{
 k = read<int>();
 while (k--) {
 n = read<LL>(), d = read<LL>(), e = read<LL>();
 LL a = 1ll, b = e*d-2ll-n, c = n;
 LL delt = b*b-a*c*4;
 if (delt < 0) {
 cout << "NO" <<endl;
 continue;
 }
 LL sqde = sqrt(delt);
 if (sqde*sqde==delt) {
 LL p = (sqde-b)/2ll/a;
 LL q = n/p;
 if (p > q) swap(p, q);
 cout <<p <<" " <<q << endl;
 } else {
 cout << "NO" << endl;
 }
 }
 return 0;
}

T3

Luogu-P8815 [CSP-J 2022] 逻辑表达式

太悲伤了,一开始想不出来怎么写,最后好不容易找到一种写法了就没考虑过时间复杂度了。。。

考前其实看了一眼 P7073 [CSP-J2020] 表达式 的,但是看了好久题解发现好麻烦,就没去研究了。。好后悔,我真的好菜

考场代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <bits/stdc++.h>
using namespace std;
const int MAXL = 1000006;
int ch[MAXL][2];
char val[MAXL];
int dep[MAXL], id[MAXL], dl[MAXL][2];
int cnt = 0;
string s;
void dfs(int l, int r, int& f) {
 if (l == r) {
 f = id[l];
 return;
 }
 int minn = MAXL, mini = -1;
 for (int i = r; i >= l; i--)
 if (s[i] == '&' || s[i] == '|') {
 if (dep[i] < minn) {
 minn = dep[i];
 mini = i;
 } else if (dep[i] == minn && s[mini] != s[i] && s[mini] == '&') {
 mini = i;
 }
 }
 f = id[mini];
 dfs(l+(s[l]=='('), mini-1, ch[id[mini]][0]);
 dfs(mini+1, r-(s[r]==')'), ch[id[mini]][1]);
}
int calc(int c) {
 if (ch[c][0] == 0){
 if (val[c] == '0') return 0;
 return 1;
 }
 int a = calc(ch[c][0]), b = calc(ch[c][1]);
 dl[c][0] = dl[ch[c][0]][0]+dl[ch[c][1]][0];
 dl[c][1] = dl[ch[c][0]][1]+dl[ch[c][1]][1];
 if (val[c] == '&') {
 if (a == 0) {
 dl[c][0] = dl[ch[c][0]][0]+1;
 dl[c][1] = dl[ch[c][0]][1];
 }
 return a&b;
 } else {
 if (a == 1) {
 dl[c][0] = dl[ch[c][0]][0];
 dl[c][1] = dl[ch[c][0]][1]+1;
 }
 return a||b;
 }
}
int main()
{
 cin >> s;
 stack<int> st;
 int maxd = 0;
 for (int i = 0; i < s.length(); i++) {
 if (s[i] == '0' || s[i] == '1') { cnt++; id[i] = cnt; }
 else if (s[i] == '|' || s[i] == '&') { cnt++; id[i] = cnt; }
 val[id[i]] = s[i];
 if (s[i] == '(') {
 st.push(i);
 } else if (s[i] == ')') {
 st.pop();
 }
 dep[i] = st.size();
 maxd = max(maxd, dep[i]);
 }
 dfs(0, s.length()-1, ch[0][0]);
 int ans = calc(ch[0][0]);
 cout << ans << endl << dl[ch[0][0]][0] << " " << dl[ch[0][0]][1] << endl;
 return 0;
}

T4

话说有没有发现今年 T3 T4 和 2020 年的解法都很像…明年一定要记得复习 CSP 2021

我承认我设的 DP 状态有点奇怪,毕竟只剩下半小时做这道题了,思路有点乱,但自我感觉答案应该是正确的。。。可是洛谷自测才 WA 50 分。

考场代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <bits/stdc++.h>
using namespace std;
int n, k;
struct node {
 int x, y;
} a[505];
bool cmp(node a, node b) {
 if (a.x != b.x) return a.x<b.x;
 return a.y<b.y;
}
int dp[605][605];
int main()
{
 cin >> n >> k;
 memset(dp, -1, sizeof dp);
 for (int i = 1; i <= n; i++) {
 cin >> a[i].x >> a[i].y;
 dp[1][i] = 0;
 }
 sort(a+1, a+1+n, cmp);
 int ans = 0;
 for (int i = 2; i <= n+k; i++) {
 for (int j = 1; j <= n; j++) {
 for (int m = 1; m < j; m++) {
 int dis = a[j].x+a[j].y-a[m].x-a[m].y-1;
 if (a[j].x < a[m].x || a[j].y < a[m].y || dis >= i || dp[i-dis-1][m]+dis>k || dp[i-dis-1][m] == -1) continue;
 dp[i][j] = dp[i-dis-1][m]+dis;
 if (dp[i][j] != -1) {
 ans = max(ans, i+k-dp[i][j]);
 }
 }
 }
 }
 cout << ans << endl;
 return 0;
}

S

T1

只会暴力。

我真的很蠢。考前复习过最短路,但无济于事。这道题求同边权的全源最短路径,我竟然只想到了 Floyd 而完全没考虑到 BFS!!!这种脑子真的不配不配不配不配不配不配不配不配拿 1=。。。。。。。。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MAXN = 2505, MAXM = 10005;
int n, m, k;
LL a[MAXN];
int tmp[MAXN];
bool cmp(int x, int y) { return a[x]>a[y];}
int f[MAXN][MAXN];
vector<int> g[MAXN];
LL ans =0,cur=0;
int t[MAXN];
bool vis[MAXN];
void dfs(int u, int step) {
 if (step == 4) {
 if (f[1][t[4]] > k+1) return ;
 ans = max(ans, cur);
 return ;
 }
 for (int i = 0; i < g[u].size(); i++) {
 if (vis[g[u][i]]) continue;
 vis[g[u][i]] = 1;
 cur += a[g[u][i]];
 t[step+1] = g[u][i];
 dfs(g[u][i], step+1);
 vis[g[u][i]] = 0;
 cur-=a[g[u][i]];
 }
}
int main()
{
 memset(f, 0x3f, sizeof f);
 scanf("%d%d%d", &n, &m, &k);
 f[1][1] = 0;
 t[1] = 1;
 for (int i = 2; i <= n; i++) scanf("%lld", &a[i]), f[i][i] = 0, tmp[i] = i;
 for (int i = 0; i < m; i++) {
 int x, y;
 scanf("%d%d", &x, &y);
 f[x][y] = f[y][x] = 1;
 }
 if (n>100 && k <= 0) {
 LL ans = 0;
 bool flag = 0;
 sort(tmp+1, tmp+1+n, cmp);
 for (int one = 1; one <= n; one++) {
 if (f[tmp[one]][1] > 1 || flag) continue;
 for (int fou = 1; fou <= n; fou++) {
 if (f[tmp[fou]][1] > 1 || one == fou || flag) continue;
 for (int two = 1; two <= n; two++) {
 if (f[tmp[one]][tmp[two]] > 1 || one == two || fou == two || flag) continue;
 for (int thr = 1; thr <= n; thr++) {
 if (f[tmp[thr]][tmp[two]] > 1 || f[tmp[thr]][tmp[fou]] > 1 || thr == one || thr == two || thr == fou || flag) continue;
 ans = a[tmp[one]]+a[tmp[two]]+a[tmp[thr]]+a[tmp[fou]];
 flag = 1;
 }
 }
 }
 }
 printf("%lld\n", ans);
 return 0;
 }

 for (int p = 1; p <= n; p++)
 for (int i = 1; i <= n; i++)
 for (int j = 1; j <= n; j++)
 f[i][j] = min(f[i][j], f[i][p]+f[p][j]);

 for (int i = 1; i <= n; i++) {
 for (int j = 1; j <= n; j++) {
 if (i == j) continue;
 if (f[i][j] <= k+1) {
 g[i].push_back(j);
 g[j].push_back(i);
 }
 }
 }
 vis[1] = 1;
 dfs(1, 0);
 printf("%lld\n", ans);
 return 0;
}

T2

唯一做出来的一道 S 组题,虽然似乎也还是拿不到 1=。

大概的思路是先根据 B 数组是否存在正数、负数、0 进行分类,然后再讨论 A 数组。具体的都在注释里了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MAXN = 100005;
int n, m, q;
LL a[MAXN], b[MAXN];
// zheng fu ling
int azs[MAXN], afs[MAXN], als[MAXN];
int bzs[MAXN], bfs[MAXN], bls[MAXN];
bool Ahz(int l, int r) { return azs[r]>azs[l-1]; } // A have zheng?
bool Ahf(int l, int r) { return afs[r]>afs[l-1]; } // A have fu?
bool Ahl(int l, int r) { return als[r]>als[l-1]; } // A have ling?
bool Bhz(int l, int r) { return bzs[r]>bzs[l-1]; }
bool Bhf(int l, int r) { return bfs[r]>bfs[l-1]; }
bool Bhl(int l, int r) { return bls[r]>bls[l-1]; }
struct SegmentTree {
 LL maxx[4*MAXN], minn[4*MAXN];
 SegmentTree() {
 for (int i = 0; i < 4*MAXN; i++) {
 maxx[i] = -1e18;
 minn[i] = 1e18;
 }
 }
 void add(int u, int l, int r, int pos, LL v) {
 if (pos < l || pos > r) return ;
 if (l == r && l == pos) {
 maxx[u] = max(maxx[u], v);
 minn[u] = min(minn[u], v);
 return;
 }
 int mid = (l+r)>>1;
 add(u<<1, l, mid, pos, v);
 add(u<<1|1, mid+1, r, pos, v);
 maxx[u] = max(maxx[u<<1], maxx[u<<1|1]);
 minn[u] = min(minn[u<<1], minn[u<<1|1]);
 }
 LL queryMax(int u, int l, int r, int x, int y) {
 if (l > y || r < x) return -1e18;
 if (l >= x && r <= y) return maxx[u];
 int mid = (l+r)>>1;
 return max(queryMax(u<<1, l, mid, x, y), queryMax(u<<1|1, mid+1, r, x, y));
 }
 LL queryMin(int u, int l, int r, int x, int y) {
 if (l > y || r < x) return 1e18;
 if (l >= x && r <= y) return minn[u];
 int mid = (l+r)>>1;
 return min(queryMin(u<<1, l, mid, x, y), queryMin(u<<1|1, mid+1, r, x, y));
 }
} az, af, bz, bf;
int main()
{
 scanf("%d%d%d", &n, &m, &q);
 for (int i = 1; i <= n; i++) {
 scanf("%lld", &a[i]);
 if (a[i] > 0) {
 az.add(1, 1, n, i, a[i]);
 } else if (a[i] < 0) {
 af.add(1, 1, n, i, a[i]);
 }
 azs[i] = azs[i-1]+(a[i]>0);
 afs[i] = afs[i-1]+(a[i]<0);
 als[i] = als[i-1]+(a[i]==0);
 }
 for (int i = 1; i <= m; i++) {
 scanf("%lld", &b[i]);
 if (b[i] > 0) {
 bz.add(1, 1, m, i, b[i]);
 } else if (b[i] < 0) {
 bf.add(1, 1, m, i, b[i]);
 }
 bzs[i] = bzs[i-1]+(b[i]>0);
 bfs[i] = bfs[i-1]+(b[i]<0);
 bls[i] = bls[i-1]+(b[i]==0);
 }
 while (q--) {
 int l, r, x, y;
 scanf("%d%d%d%d", &l, &r, &x, &y);

 if (Bhz(x, y) && Bhl(x, y) && Bhf(x, y)) {
 if (Ahl(l, r)) {
 printf("0\n");
 } else {
 LL ans = -1e18;
 // A最小正数*B最小负数
 if (Ahz(l, r))
 ans = az.queryMin(1, 1, n, l, r)*bf.queryMin(1, 1, m, x, y);
 // A最大负数*B最大正数
 if (Ahf(l, r))
 ans = max(ans, af.queryMax(1, 1, n, l, r)*bz.queryMax(1, 1, m, x, y));
 printf("%lld\n", ans);
 }
 } else if (Bhz(x, y) && Bhl(x, y)) {
 if (Ahz(l, r) || Ahl(l, r)) {
 printf("0\n");
 } else {
 // A最大负数*B最大正数
 printf("%lld\n", af.queryMax(1, 1, n, l, r)*bz.queryMax(1, 1, m, x, y));
 }
 } else if (Bhz(x, y) && Bhf(x, y)) {
 if (Ahl(l, r)) {
 printf("0\n");
 } else{
 LL ans = -1e18;
 if (Ahz(l, r))
 ans = az.queryMin(1, 1, n, l, r)*bf.queryMin(1, 1, m, x, y);
 if (Ahf(l, r))
 ans = max(ans, af.queryMax(1, 1, n, l, r)*bz.queryMax(1, 1, m, x, y));
 printf("%lld\n", ans);
 }
 } else if (Bhl(x, y) && Bhf(x, y)) {
 if (Ahf(l, r) || Ahl(l, r)) {
 printf("0\n");
 } else {
 // A 最小正数*B最小负数
 printf("%lld\n", az.queryMin(1, 1, n, l, r)*bf.queryMin(1, 1, m, x, y));
 }
 } else if (Bhz(x, y)) {
 if (Ahz(l, r)) {
 // A 最大正数*B最小正数
 printf("%lld\n", az.queryMax(1, 1, n, l, r)*bz.queryMin(1, 1, m, x, y));
 } else if (Ahl(l, r)) {
 printf("0\n");
 } else {
 // A 最大负数*B最大正数
 printf("%lld\n", af.queryMax(1, 1, n, l, r)*bz.queryMax(1, 1, m, x, y));
 }
 } else if (Bhf(x, y)) {
 if (Ahf(l, r)) {
 // A 最小负数*B最大负数
 printf("%lld\n", af.queryMin(1, 1, n, l, r)*bf.queryMax(1, 1, m, x, y));
 } else if (Ahl(l, r)) {
 printf("0\n");
 } else {
 // A 最小正数*B最小负数
 printf("%lld\n", az.queryMin(1, 1, n, l, r)*bf.queryMin(1, 1, m, x, y));
 }
 } else if (Bhl(x, y)) {
 printf("0\n");
 }
 }
 return 0;
}

T3

直接放弃。

T4

纯骗分的。还是不会做。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
typedef long long LL;
int n, q, k;
LL v[MAXN], ans[MAXN];
int flo[2005][2005];
struct node {
 int a;
 LL v;
};
int main()
{
 scanf("%d%d%d", &n, &q, &k);
 memset(flo, 0x3f, sizeof flo);
 for (int i = 1; i <= n; i++) scanf("%lld", &v[i]), flo[i][i] = 0;
 for (int i = 0; i < n-1; i++) {
 int a, b;
 scanf("%d%d", &a, &b);
 flo[a][b] = flo[b][a] = 1;
 }
 for (int k = 1; k <= n; k++)
 for (int i = 1; i <= n; i++)
 for (int j = 1; j <= n; j++)
 flo[i][j] = min(flo[i][j], flo[i][k]+flo[k][j]);
 while (q--) {
 int s, t;
 scanf("%d%d", &s, &t);
 queue<node> q;
 q.push({s, v[s]});
 memset(ans, 0x3f, sizeof ans);
 while (!q.empty()) {
 int f = q.front().a;
 LL va = q.front().v;
 q.pop();
 if (va > ans[f]) continue;
 ans[f] = min(ans[f], va);
 if (f == t)
 continue;
 for (int i = 1; i <= n; i++) {
 if (i == f || flo[i][f] > k) continue;
 q.push({i, va+v[i]});
 }
 }
 printf("%lld\n", ans[t]);
 }
 return 0;
}

ARC101B Median of Medians - 中位数

2022年10月23日 08:00

D - Median of Medians

题意

给定一个整数序列 $a[1],a[2],….a[n]$,那么对于 $a$ 序列的任意一个连续子序列 $a[L],a[L+1],……a[R]$,其中 $1<=L<=R<=n$, 求出该连续子序列的中位数,记为 $b[L][R]$。

显然 $b$ 数组共有 $n*(n+1)/2$ 个整数。

输出 $b$ 数组的中位数。

分析

关于中位数有一个 Trick:

  • 我们二分一个数 $mid$,对于原序列中 $\ge mid$ 的数,我们标记为 $1$;反之,对于 $< mid$ 的数,我们标记为 $−1$。
  • 标记结束后,如果一个区间内的标记和大于等于 $0$,说明中位数大于等于 $mid$,那么向右二分;反之向左。

对于本题,我们对 $b$ 数组二分它的中位数 $mid$,并按 $mid$ 对 $a$ 数组进行 $+1,-1$ 标记。然后问题就变为了:统计有多少个区间的标记和 $\ge 0$。

记这个区间数为 $cnt$,若 $cnt\ge \lfloor \frac{n(n+1)/2+1}{2} \rfloor$,说明 $b$ 数组实际中位数 $\ge mid$,向右二分。否则向左二分。

怎么求有多少个区间的标记和 $\ge 0$ 呢?我们可以做一个前缀和 $s$,统计 $i < j$ 且 $s[i] \le s[j]$ 的个数。这是一个二维偏序问题,可以搭配树状数组解决。

$O(nlog^2n)$。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int n;
int a[100005], b[100005], t[100005], s[100005], tree[200005];
int lowbit(int x) { return x&(-x); }
void add(int x) {
 while (x <= 2*n+1) {
 tree[x]++;
 x += lowbit(x);
 }
}
ll query(int x) {
 ll s = 0;
 while (x) {
 s += tree[x];
 x -= lowbit(x);
 }
 return s;
}
int read(){
 int s = 0, f = 1;
 char c = getchar();
 while (!isdigit(c)) {
 if (c == '-') f = -1;
 c = getchar();
 }
 while (isdigit(c)) {
 s = s*10+c-'0';
 c = getchar();
 }
 return s*f;
}
bool check(int x) {
 ll cnt = 0;
 for (int i = 1; i <= n; i++) {
 t[i] = a[i]>=x ? -1 : 1;
 s[i] = s[i-1]+t[i];
 }
 memset(tree, 0, sizeof tree);
 add(n+1);
 for (int i = 1; i <= n; i++) {
 cnt += query(s[i]+n);
 add(s[i]+n+1);
 }
 return cnt >= (ll)n*(n+1)/4+1;
}
int main()
{
 n = read();
 for (int i = 1; i <= n; i++) a[i] = b[i] = read();
 sort(b+1, b+1+n);
 int L = 0, R = n+1;
 while (L + 1 < R) {
 int M = (L+R)>>1;
 if (check(b[M])) {
 R = M;
 } else {
 L = M;
 }
 }
 cout << b[L] << endl;
 return 0;
}

@ 2022/10/15 SM 模拟赛。

最短路笔记

2022年10月1日 08:00

又是一年 CSP 复赛,已经一年没写过最短路了,赶紧复习一下。

Floyd-Warshall 算法

Floyd 算法是用来求所有结点对最短路的。适用于所有不含负环的图。

这个算法运用了 DP 的思想。首先定义 f[k][i][j] 表示只允许经过结点 $1, 2, \cdots k$,结点 $i$ 到结点 $j$ 的最短路长度。初始化时,f[k][i][i] = 0,其他赋值为 $+\infty$。可以有 f[k][i][j] = min(f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j])f[k-1][i][j] 表示不经过 $k$ 点的最短路径,f[k-1][i][k] + f[k-1][k][j] 表示经过 $k$ 点的最短路径)。这时可以发现,数组的第一维是可以忽略的,所以直接写成 f[i][j] = min(f[i][j], f[i][k] + f[k][j])

时间、空间复杂度均为 $O(N^3)$。

实现

1
2
3
4
for (int k = 1; k <= n; k++)
 for (int i = 1; i <= n; i++)
 for (int j = 1; j <= n; j++)
 f[i][j] = min(f[i][j], f[i][k] + f[k][j]);

无向图的最小环问题

Luogu-P6175 无向图的最小环问题

记原图中 $i,j$ 之间边的边权为 $val\left(i,j\right)$。

我们注意到 Floyd 算法有一个性质:在最外层循环到点 $k$ 时(尚未开始第 $k$ 次循环),最短路数组 $dis$ 中,$dis_{i,j}$ 表示的是从 $i$ 到 $j$ 且仅经过编号在 $\left[1, k\right)$ 区间中的点的最短路。

由最小环的定义可知其至少有三个顶点,设其中编号最大的顶点为 $w$,环上与 $w$ 相邻两侧的两个点为 $i,j$,则在最外层循环枚举到 $k=w$ 时,该环的长度即为 $dis_{i,j}+val\left(j,w\right)+val\left(w,i\right)$。

故在循环时对于每个 $k$ 枚举满足 $i<k,j<k$ 的 $(i,j)$,更新答案即可。

实现

1
2
3
4
5
6
7
8
9
int ans = INF;
for (int k = 1; k <= n; k++) {
 for (int i = 1; i < k; i++) {
 for (int j = i + 1; j < k; j++)
 ans = min(ans, dis[i][j] + val[j][k] + val[k][i]);
 for (int i = 1; i <= n; i++)
 for (int j = 1; j <= n; j++)
 dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}

顺便说说有向图的最小环求法:令 f[i][i] = +INF,然后 Floyd 求最短路。

Bellman-Ford 算法

Bellman-Ford 算法解决的是一般情况下的单源最短路径问题,边的权重可以为负值。它可以判断是否存在一个从源节点可以到达的负环。

先解释一下松弛操作。对于边 $(u, v)$,松弛操作表示 $dis(v) = \min(dis(v), dis(u)+w(u, v))$。含义很简单,就是用 $S\rightarrow u \rightarrow v$(其中 $S\rightarrow u$ 的路径取最短路)这条路径去更新 $v$ 的最短路的长度。

Bellman-Ford 算法就是不停地做松弛操作,当一次循环中没有成功地松弛操作时,算法停止。

每次循环是 $O(m)$ 的,在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 $+1$,而最短路的边数最多为 $n-1$,因此整个算法最多执行 $n-1$ 轮松弛操作。故总时间复杂度为 $O(nm)$。

但还有一种情况,如果从 $S$ 点出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 $n-1$ 轮,因此如果第 $n$ 轮循环时仍然存在能松弛的边,说明从 $S$ 点出发,能够抵达一个负环。

注意:需要注意的是,以 $S$ 点为源点跑 Bellman-Ford 算法时,如果没有给出存在负环的结果,只能说明从 $S$ 点出发不能抵达一个负环,而不能说明图上不存在负环。如果需要判断整个图上是否存在负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为 0 的边,然后以超级源点为起点执行 Bellman-Ford 算法。

实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
vector<edge> G[MAXN];
int dis[MAXN];
bool bellmanford() {
 memset(dis, 0x3f, sizeof dis);
 dis[s] = 0;
 bool flag; // 判断有没有松弛
 for (int i = 0; i < n; i++) {
 flag = false;
 for (int u = 1; u <= n; u++) {
 if (dis[u] == 0x3f3f3f3f) continue;
 // 无穷大与常数加减仍然为无穷大
 // 因此最短路长度为 inf 的点引出的边不可能发生松弛操作
 for (auto j : G[u]) {
 int v = j.to, w = j.w;
 if (dis[v] > dis[u] + w) {
 dis[v] = dis[u] + w;
 flag = true;
 }
 }
 }
 // 没有可以松弛的边时就停止算法
 if (!flag) break;
 }
 return flag; // 第 n 轮循环仍然可以松弛时(flag==true)说明 s 点可以抵达一个负环
}

队列优化:SPFA

关于 SPFA:它死了

很多时候我们并不需要那么多无用的松弛操作。显然,只有上一次被松弛的结点所连接的边,才有可能引起下一次的松弛操作。

那么我们用队列来维护“哪些结点可能会引起松弛操作”,就能只访问必要的边了。

SPFA 也可以用于判断 $s$ 点是否能抵达一个负环,只需记录最短路经过了多少条边,当经过了至少 $n$ 条边时,说明 $s$ 点可以抵达一个负环。

慎用!SPFA 在最坏情况下时间复杂度和 Bellman-Ford 一样为 $O(nm)$。如果没有负权边时,一定要使用 Dijkstra 算法。

实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
vector<edge> G[MAXN];
bool vis[MAXN];
int dis[MAXN], cnt[MAXN]; // cnt 记录最短路经过的边数
bool SPFA() {
 memset(dis, 0x3f, sizeof dis);
 dis[s] = 0;
 queue<int> q;
 q.push(s);
 while (!q.empty()) {
 int u = q.front();
 q.pop();
 vis[u] = 0;
 for (auto i : G[u]) {
 int v = i.to, w = i.w;
 if (dis[v] > dis[u] + w) {
 dis[v] = dis[u] + w;
 cnt[v] = cnt[u] + 1;
 // 在不经过负环的情况下,最短路至多经过 n - 1 条边
 // 因此如果经过了多于 n 条边,一定说明经过了负环
 if (cnt[v] >= n) return true;
 if (!vis[v]) {
 vis[v] = 1;
 q.push(v);
 }
 }
 }
 }
 return false;
}

Dijkstra 算法

Dijkstra 算法要求所有边的权重都为非负值,运用贪心策略。我们把结点分为两个集合:已确定最短路长度的点集 $S$ 和未确定最短路长度的点集 $T$。算法重复从 $T$ 中选择最短路径估计最小的结点 $u$,将 $u$ 加入到集合 $S$,然后对所有从 $u$ 发出的边进行松弛。

暴力 Dijkstra 实现需要每次暴力寻找一个最小值,共寻找 $n$ 次,为 $O(n^2)$。每条边可能要进行一次松弛,为 $O(m)$。因此时间复杂度是 $O(n^2+m) = O(n^2)$。

实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int n, m, s;
struct edge {
 int to, w;
};
vector<edge> G[MAXN];
bool used[MAXN];
int dis[MAXN];
void dijkstra() {
 memset(dis, 0x3f, sizeof dis);
 dis[s] = 0;
 for (int i = 1; i <= n; i++) {
 int minn = n + 1;
 for (int j = 1; j <= n; j++)
 if (!used[j] && dis[j] < dis[minn]) minn = j;
 used[minn] = 1;
 for (auto j : G[minn]) dis[j.to] = min(dis[j.to], dis[minn] + j.w); // RELAX
 }
}

优先队列优化

可以用优先队列来寻找最短路长度的最小值,时间复杂度优化为 $O(m\log m)$。

实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct edge {
 int to, w;
 bool operator>(const edge b) { return w > b.w; }
};
vector<edge> G[MAXN];
bool used[MAXN];
int dis[MAXN];
priority_queue<edge, vector<edge>, greater<>> pq;
void dijkstra() {
 memset(dis, 0x3f, sizeof dis);
 dis[s] = 0;
 pq.push({s, 0});
 while (!pq.empty()) {
 edge t = pq.top();
 pq.pop();
 if (used[t.to]) continue;
 used[t.to] = 1;
 for (auto i : G[t.to]) {
 int v = i.to, w = i.w;
 if (dis[v] > dis[t.to] + w) {
 dis[v] = dis[t.to] + w;
 pq.push({v, dis[v]});
 }
 }
 }
}

要注意 priority_queue 默认是大根堆。换成小根堆需要重载 > 运算符,并使用 priority_queue<edge, vector<edge>, greater<>> pq;

在松弛成功后,需要重新修改结点 v 的优先级,但是 STL 中优先队列不允许这样的操作。所以我们只能将新的元素插入优先队列(这样做不会影响正确性,因为新的元素的最短路长度更小,会更早出队),为了避免结点重复扩展,如果发现新取出的结点已经扩展过(used[])就应该扔掉。另一种方法是把 if (used[t.to]) 换成 if (t.w == dis[t.to])dis[t.to] 一定是当前最短的)。

不同方法的比较

最短路算法 Floyd Bellman-Ford Dijkstra
最短路类型 每对结点之间的最短路 单源最短路 单源最短路
作用于 任意图 任意图 非负权图
能否检测负环? 不能
推荐作用图的大小 中/小 大/中
时间复杂度 $O(N^3)$ $O(NM)$ $O(M\log M)$

注:表中的 Dijkstra 算法在计算复杂度时均用 priority_queue 实现。

输出方案

比如 Floyd 就要记录 pre[i][j] = k,Bellman-Ford 和 Dijkstra 一般记录 pre[v] = u


UPDATE after CSP-S 2022:对于我这种不会变通的 sha b*,必须得做个笔记:

对于一些特殊的图,也可以用 BFS 求最短路:

  • 在一个无权图上求从起点到其他所有点的最短路径。进一步地,用这个方法也可以 $O(nm)$ 求全源最短路径[CSP-S 2022] 假期计划 就用到了这一个方法,但我却使用了 Floyd !!!从一开始就葬送了这道题!!!
  • 在一个边权为 0/1 的图上求最短路(0-1 BFS,双端队列 BFS)例题:「BalticOI 2011 Day1」打开灯泡 Switch the Lamp On

参考资料

CSP-J 2022 游记

2022年9月25日 18:35

CSP-J1/S1

这次和初一的时候一样,在自己学校考试,舒服。

感觉题目偏简单,但是出了很多奇怪的题目。。。(网上的 dalao 都说有一堆错误?

估分 J 组 90,S 组 68,应该也许能过吧。

结果 J 组 92,S 组 65.5。

GD 出分数线一如既往地咕咕咕咕咕

本来 S 组应该才勉强压线(ZJ 64),但是今年 GD 貌似加了很多机位,分数线才 55。于是初三的我第一次进了 S 组复赛。。。

CSP-J2/S2

Day -?

本来说要去东莞考的,后来又改到石中了,有点失望。

Day 0

周五。全市封校。:) 上次被疫情搞得回不了家还是初一下学期的时候。

Day 1

早上五点四十起床,坐大巴去石中。

J 组,前两题大概花了一小时。T3 和 2020-T3 如出一辙,考前一晚看过,但是没看懂。于是考场上花了很久思考,最后想到一种实现方式,就没有思考过时间复杂度直接开写,结果大样例超时,一共花了一小时。最后花了十几分钟乱写 T4,总体自我感觉良好。

中午饭还算挺香,饭后在草丛里捡了个篮球,打了十几分钟,一个过路老师阴沉地说:“中午不准打球。”(鬼知道我在石中听到这句话多少次了。。。)

S 组,同桌是 czz 大神。慢慢地看 T1,发现还是没有头绪,打了个暴力。然后看到 T2,感觉有点希望,然后发现好水,赶紧做了,大喜。再看 T3,直接放弃。T4 再打了个暴力。这时候就到了六点,静坐了半小时,出考场。

考完之后感觉题简单了,运气好也许能冲 1=?


Day 2+

洛谷自测 J 组,发现 T3 T4 各 50 分,网上都说 J 组很简单,感觉 1= 就这样没了。。。。

S 组还好,暴力分都骗到了,T2 也满了,但是网上说 S 组也很简单,感觉 1= 也没了。。。。

那一天我在思考会不会真的一个 1= 都没了。真无语。

诶,这次还是有很多需要总结的。

  • 考前看上上年的试题。(今年 J 组 T3 T4 真的和 2020 很像
  • 带点脑子,多练题。S 组 T1 甚至不知道无权图可以用 BFS 求全源最短路,只写了一个 Floyd。为什么这么简单的我都想不到呢?另一个方面来说,平时好像真的从来没练到过这个知识点。。。做的题目真的太少太少了。
  • 考虑时间复杂度。J 组 T3 做的时候觉得这种题想到了就对了,时间应该没问题,所以就没认真看范围。然而事实上它根本就不是 J-2021-T3 那种大模拟类型的题。。。

Luogu CSP-J: 100+100+50+50=300

Luogu CSP-S: 55+100+20=175


CCF J:100+100+50+65=315

CCF S:50+100+0+20=170

SHABI CCF!!!SHABI CCF!!!SHABI CCF!!!SHABI CCF!!!SHABI CCF!!!

CSP-S T3 全输出 NO 45 分!!!!!!!

CSP-S T3 全输出 NO 45 分!!!!!!!

CSP-S T3 全输出 NO 45 分!!!!!!!

谁能想到多组数据还能用这种骗分方法!!!!!!而且还有 45 分!!!!!!

CCF WDNMD!


初三的生活实在是太累了,哪怕是偶尔闭上眼睛,公式单词诗句也会飘上眼皮。每天都是强打精神听课,老师的话从脑子里穿过,实际上还在睡觉。什么时候,这一切才能结束呢?

CSP 结束了,三年的初中信息组生涯也结束了。诶,不知道该说什么。

❌
❌