网络流笔记

最大流 容量:$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$ 在层次图中与源点的距离。 层次图:在原残量网络中按照每个节点的层次来分层,只保留相邻两层的节点的图,满载(即流量等于容量)的边不存在于层次图中。 流的三个重要性质: 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即,$f(u,v)\leq c(u,v)$ 斜对称性:每条边的流量与其相反边的流量之和为 0,即 $f(u,v)=-f(v,u)$ 流守恒性:从源点流出的流量等于汇点流入的流量,即 $\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)$)。 增广路指一条从 $s$ 到 $t$ 的路径,路径上每条边残余容量都为正。把残余容量为正($w(u, v) \gt 0$)的边设为可行边,然后搜索寻找增广路。 找到一条增广路后,这条路能够增广的流值由路径上边的最小残留容量 $w(u, v)$(记为 $flow$)决定。将这条路径上每条边的 $w(u, v)$ 减去 $flow$。最后,路径上每条边的反向边残留容量要加上 $flow$。 为什么增广路径上每条边的反向边的残留容量值要加上 $flow$?因为斜对称性,残量网络=容量网络-流量网络,容量网络不变时,流量网络上的边的流量增加 $flow$,反向边流量减去 $flow$,残量网络就会发生相反的改变。从另一个角度来说,这个操作可以理解为「退流」,给了我们一个反悔的机会,让增广路的顺序不受限制。 增广路算法好比是自来水公司不断的往水管网里一条一条的通水。 这个算法基于増广路定理:网络达到最大流当且仅当残留网络中没有増广路。 建图技巧:从 $2$ 开始对边编号,这样 $i$ 的反向边就是 $i \oplus 1$。 用 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; } 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 算法的流程如下。 在 $G_f$ 上 BFS 出层次图 $G_L$。 在 $G_L$ 上 DFS 出阻塞流 $f_b$。 将 $f_b$ 并到原先的流 $f$ 中,即 $f \leftarrow f + f_b$。 重复以上过程直到不存在从 $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$。跑网络最大流,将所有正权值之和减去最大流,即为答案。 几个小结论来证明: 每一个符合条件的子图都对应流量网络中的一个割。因为每一个割将网络分为两部分,与 $s$ 相连的那部分满足没有边指向另一部分,于是满足上述条件。这个命题是充要的。 最小割所去除的边必须与 $s$ 和 $t$ 其中一者相连。因为否则边权是 $\infty$,不可能成为最小割。 我们所选择的那部分子图,权值和 $=$ 所有正权值之和 $-$ 我们未选择的正权值点的权值之和 $+$ 我们选择的负权值点的权值之和。当我们不选择一个正权值点时,其与 $s$ 的连边会被断开;当我们选择一个负权值点时,其与 $t$ 的连边会被断开。断开的边的边权之和即为割的容量。于是上述式子转化为:权值和 $=$ 所有正权值之和 $-$ 割的容量。 于是得出结论,最大权值和 $=$ 所有正权值之和 $-$ 最小割 $=$ 所有正权值之和 $-$ 最大流。 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(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; } OI Wiki 最大权闭合子图 - kikokiko 网络流 - Siyuan 最详细(也可能现在不是了)网络流建模基础 - ~victorique~

2023/7/29
articleCard.readMore

网络流题集

最大流 三倍经验: Luogu-P1402 酒店之王 / Luogu-P2891 [USACO07OPEN] Dining G 一眼丁真建图:S->练习册->书->答案->T 然而是错的。很明显,书有可能被多次匹配,与题意不符。 正确的建图:S->练习册->书(拆点)->答案->T 为什么中间层的书要拆点呢?因为一本书不能被重复选用。我们的目的是保证一本书流出的流量只能是 $1$。所以我们把每个代表书的点拆成两个点,左边的点和练习册连边,右边的点和答案连边;左右对应点之间也要连一条容量为 $1$ 的边。 定理:最小路径覆盖数=$|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$ 的边(他们之间的边只从白连到黑,避免重复计算)。答案为全局和减去最小割。 原点向所有狼连容量 $INF$ 的边,所有羊向汇点连容量 $INF$ 的边,所有点向四周连容量为 $1$ 的边。 最小割即为答案,因为狼和羊之间的边都被割掉了。 二者取其一模型。 考虑点集 ${u, v, w}$ 对集合 $A$ 的贡献,加入其中一者被割进 $B$,那这个点集就没有贡献,即点集与 $A$ 的连边要割掉。 用一个虚点 $x$ 从 $S$ 连一条边代表贡献,如果其中一个点被割进了集合 $B$,那这条代表贡献的边也要割掉,而 $x$ 到 $u, v, w$ 的边不能断开。所以 $(S, x)$ 的容量为 $c$,$(x, u), (x, v), (x, w)$ 的容量都是 $INF$(保证不被断开)。 答案为总收益减去最小割。 两倍经验:Luogu-P3410 拍照 最大权值闭合图模型。 如果没有租机器,那这道题就是纯最大权值闭合图模型。 图中对于工作和它所需的机器之间边的容量为 $INF$,所以这条边不可能被割,意义即为选择这个工作就必须购买这个机器。那么租用就可以将这条边的容量改为 $b_{ij}$,可以用 $b_{ij}$ 割掉这条边,表示选择这个工作后,可以花费 $b_{ij}$ 的代价代替购买机器。

2023/7/29
articleCard.readMore

CF-559C Gerald and Giant Chess

CF-559C Gerald and Giant Chess / AtCoder DP-Y Grid 2 给定一个 $H*W$ 的棋盘,棋盘上只有 $N$ 个格子是黑色的,其他格子都是白色的。在棋盘左上角有一个卒,每一步可以向右或者向下移动一格,并且不能移动到黑色格子中。求这个卒从左上角移动到右下角,一共有多少种可能的路线。 $(1 ≤ h, w ≤ 105, 1 ≤ n ≤ 2000)$ $O(hw)$ 的暴力 DP 很好想,但是过不了。 假设没有障碍,从 $(1, 1)$ 到 $(i, j)$ 的方案数是 $C_{i+j-2}^{i-1}$(等于 $C_{i+j-2}^{j-1}$)。可以这么理解:可以用 $D, R$ 来表示一条路径,那么从 $(1, 1)$ 到 $(i, j)$ 的路径中有 $i-1$ 个 $D$ 和 $j-1$ 个 $R$。于是问题转化为从 $i+j-2$ 个位置中选 $i-1$ 个放 $D$ 的方案数。 如果有一个障碍,从正面统计方案数很困难,正难则反,考虑将总的方案数减去经过障碍的方案数。假设障碍的位置是 $(x, y)$,终点是 $(h, w)$,经过障碍的方案数就是 $C_{x+y-2}^{x-1} * C_{h-x+w-y}^{h-x}$(乘法原理)。 有多个障碍怎么办呢?联想到容斥原理,将总的方案数减去至少经过 $1$ 个障碍的,加上 $2$ 个的,减去 $3$ 个的……但这样做复杂度很高。 可以设 $dp_i$ 表示从 $(1, 1)$ 到 $(x_i, y_i)$ 且中途不经过其它障碍的方案数。令终点 $(h, w)$ 为第 $n+1$ 个障碍,求的答案就是 $dp_{n+1}$。 依然是正难则反,得出方程:$dp_i = C_{x_i+y_i-2}^{x_i-1}-\sum_{j=1}^{i-1}dp_j*C_{x_i-x_j+y_i-y_j}^{x_i-x_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 29 30 31 32 33 34 35 36 37 38 39 40 #include <bits/stdc++.h> using namespace std; using LL = long long; const LL p = 1000000007; int h, w, n; LL dp[3005], fac[300010], inv[300010], invf[300010]; LL C(int n, int k) { if (n == k || k == 0) return 1; return fac[n] * invf[k] % p * invf[n - k] % p; } LL calc(int x1, int y1, int x2, int y2) { return C(x2 + y2 - x1 - y1, x2 - x1); } struct za { int r, c; bool operator<(const za& b) { if (r != b.r) return r < b.r; return c < b.c; } } a[3005]; int main() { ios::sync_with_stdio(false); inv[1] = invf[1] = fac[1] = 1; for (int i = 2; i <= 300000; i++) { inv[i] = (p - p / i) * inv[p % i] % p; invf[i] = invf[i - 1] * inv[i] % p; fac[i] = (fac[i - 1] * i) % p; } cin >> h >> w >> n; for (int i = 1; i <= n; i++) cin >> a[i].r >> a[i].c; sort(a + 1, a + 1 + n); a[++n] = {h, w}; for (int i = 1; i <= n; i++) { dp[i] = calc(1, 1, a[i].r, a[i].c); for (int j = 1; j < i; j++) { if (a[j].r <= a[i].r && a[j].c <= a[i].c) dp[i] = (dp[i] - dp[j] * calc(a[j].r, a[j].c, a[i].r, a[i].c) % p + p) % p; } } cout << dp[n] << endl; return 0; } Orz:AT4546 题解 - GaryH

2023/5/21
articleCard.readMore

初等数论入门

我也不知道这是从哪本书上抠来的? 定义 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$,分两种情况: $b \lt a/2$, 经过一次辗转得到 $(b,a\bmod b)$,$\max(a,b)$ 至少缩小一半。 $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)$。 首先证明每个 $a,b$ 的线性组合是 $d$ 的倍数。首先注意到由最大公因子的定义,有 $d\mid a$ 且 $d\mid b$,每个 $a,b$ 的线性组合具有形式 $ma+nb$,其中 $m,n$ 是整数。 由定理 2,只要 $m,n$ 是整数,$d$ 就整除 $ma+nb$,因此,$ma+nb$ 是 $d$ 的倍数。 现在证明每一个 $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$ 是整数。 证明任何一对整数 $(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)$ 就是方程的解。 证明方程的任何一个解都具有 $(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$。 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; } 多元一次不定方程的一组解:求 $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; } 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$ 的同余满足下面的性质: 自反性。若 $a$ 是整数,则 $a\equiv a(mod\ m)$。 对称性。若 $a$ 和 $b$ 是整数,且 $a\equiv b(mod\ m)$,则 $b\equiv a(mod\ m)$。 传递性。若 $a,b,c$ 是整数,且 $a\equiv b(mod\ m)$ 和 $b\equiv c(mod\ m)$,则 $a\equiv c(mod\ m)$。 证明: 因为 $m\mid(a-a)=0$,所以 $a=a(mod\ m)$。 若 $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)$。 若 $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$。 证明: 若 $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$。 若 $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)$,则 $a+c\equiv b+c(mod\ m)$ $a-c\equiv b-c(mod\ m)$ $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)$,则: $a+c\equiv b+d(mod\ m)$ $a-c\equiv b-d(mod\ m)$ $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)$。 求逆的三种算法: 扩展欧几里得算法解同余方程,求单个逆。$ax\equiv 1(mod\ m)$ 等价于求二元线性不定方程的解:$ax+my=1$,其中 $(a,m)=1$ 是有逆的前提。只需要用扩展欧几里得算法求即可。 费马小定理,求单个逆 定理 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}$。 若 $p$ 是素数,$n\lt p$,线性递推求 $1$ 至 $n$ 在模 $p$ 意义下的乘法逆元 首先,$i=1$ 的逆是 $1$。下面求 $i\gt 1$ 的逆,用递推法。 用带余除法 $p = k·i + r$,其中 $0\le r \lt i$,故 $k·i + r \equiv 0 (mod\ p)$ 在等式两边乘 $i^{-1}r^{-1}$,即 $(k·i + r)·i^{-1}r^{-1} \equiv 0 (mod\ p)$,即 $kr^{-1} + i^{-1} \equiv 0 (mod\ p)$ 移项得 $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; } 数学:Exgcd 与乘法逆元的漫步 - Aw顿顿の小窝 · Linear Expectation 线性期望 【数论】Exgcd/乘法逆元 - 题单

2023/5/5
articleCard.readMore

Luogu-P4755 Beautiful Pair

Luogu-P4755 Beautiful Pair 小 D 有个数列 ${a}$,当一个数对 $(i,j)$($i \le j$)满足 $a_i$ 和 $a_j$ 的积不大于 $a_i, a_{i+1}, \ldots, a_j$ 中的最大值时,小 D 认为这个数对是美丽的。请你求出美丽的数对的数量。 $1\le n\le{10}^5$,$1\le a_i\le{10}^9$。 对 ST 表不熟悉! 更 zz 的是,对 lower_bound 和 upper_bound 理解有问题,来复习一下小学知识:lower_bound 是找到“大于等于”的位置,upper_bound 是“大于”。写这道题的时候找小于某数的位置莫名其妙地用了 lower_bound,更没有 -1,完全是随手写的,半天也没察觉到这里有问题。 综上,我是 zz。 考虑分治(据说这是套路),我们找出一个区间 $[l, r]$ 内的最大值位置 $mid$,然后统计所有跨过 $mid$ 的答案,再递归处理 $[l, mid-1], [mid+1, r]$。假设 $mid$ 左边的数是 $a_i$,右边的数是 $a_j$,根据题目得 $a_i * a_j \le a_{mid}$,即 $a_j \le \lfloor\frac{a_{mid}}{a_i}\rfloor$。那么我们枚举 $a_i$,然后用主席树统计右区间内小于 $\lfloor\frac{a_{mid}}{a_i}\rfloor$ 的数的个数。 注意每次要枚举左右区间中长度较小的那个,这样可以做到 $O(n\log^2n)$。否则会被卡成 $O(n^2\log n)$。 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 #include <bits/stdc++.h> using namespace std; const int N = 100005, LOGN = 18; int n, a[N], b[N], bn, f[N][LOGN], logn[N], root[N], cnt; struct node { int sum, ls, rs; } t[N*25]; void update(int &rt, int pre, int pos, int l, int r) { rt = ++cnt; t[rt] = t[pre]; t[rt].sum++; if (l == r) return; int mid = (l+r)>>1; if (pos <= mid) update(t[rt].ls, t[pre].ls, pos, l, mid); else update(t[rt].rs, t[pre].rs, pos, mid+1, r); } int query(int rt, int pre, int x, int y, int l, int r) { if (l >= x && r <= y) return t[rt].sum-t[pre].sum; int mid = (l+r)>>1; int ret = 0; if (x <= mid) ret += query(t[rt].ls, t[pre].ls, x, y, l, mid); if (y > mid) ret += query(t[rt].rs, t[pre].rs, x, y, mid+1, r); return ret; } void buildst() { logn[1] = 0; logn[2] = 1; for (int i = 3; i < N; i++) logn[i] = logn[i/2]+1; for (int i = 1; i <= n; i++) f[i][0] = i; for (int t = 1; t < LOGN; t++) { for (int i = 1; i+(1<<t)-1 <= n; i++) { if (a[f[i][t-1]] > a[f[i+(1<<(t-1))][t-1]]) f[i][t] = f[i][t-1]; else f[i][t] = f[i+(1<<(t-1))][t-1]; } } } int getmax(int l, int r) { int t = logn[r-l+1]; // NOT f[l+(1<<(t-1))-1][t] if (a[f[l][t]] > a[f[r-(1<<t)+1][t]]) return f[l][t]; return f[r-(1<<t)+1][t]; } long long ans = 0; void solve(int l, int r) { if (l > r) return; if (l == r) { ans += (a[l]==1); return; } int mid = getmax(l, r); if (mid-l+1 <= r-mid+1) { // 枚举左半区间 for (int i = l; i <= mid; i++) { // 不能用lowerbound int idx = upper_bound(b+1, b+1+bn, a[mid]/a[i])-b-1; if (idx != 0) ans += query(root[r], root[mid-1], 1, idx, 1, bn); } } else { // 枚举右半区间 for (int i = mid; i <= r; i++) { int idx = upper_bound(b+1, b+1+bn, a[mid]/a[i])-b-1; if (idx != 0) ans += query(root[mid], root[l-1], 1, idx, 1, bn); } } solve(l, mid-1); solve(mid+1, r); } int main() { ios::sync_with_stdio(false); cin >> n; for (int i = 1; i <= n; i++) cin >> a[i], b[i] = a[i]; sort(b+1, b+1+n); bn = unique(b+1, b+1+n)-b-1; root[0] = ++cnt; buildst(); for (int i = 1; i <= n; i++) { int id = lower_bound(b+1, b+1+bn, a[i])-b; update(root[i], root[i-1], id, 1, bn); } solve(1, n); cout << ans << endl; return 0; }

2023/4/30
articleCard.readMore

ABC209F Deforestation

ABC209F Deforestation 题意:给出 $n$ 棵树的高度,砍第 $i$ 棵树的花费是 $h_i+h_{i-1}+h_{i+1}$,求有多少种方案能使得砍完所有树的总代价最小。 砍一棵树的代价只与相邻的树高度有关。下面研究砍 $h_i$ 与 $h_{i+1}$ 的先后顺序对答案的影响。 先砍 $h_i$ 后砍 $h_{i+1}$:$h_i+h_{i-1}+h_{i+1}+h_{i+1}+h_{i+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

2023/4/28
articleCard.readMore

CDQ 分治笔记

基本思想 CDQ 分治的基本思想十分简单。如下: 我们要解决一系列问题,这些问题一般包含修改和查询操作,可以把这些问题排成一个序列,用一个区间 $[L,R]$ 表示。 分。递归处理左边区间 $[L,M]$ 和右边区间 $[M+1,R]$ 的问题。 治。合并两个子问题,同时考虑到 $[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; } 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]动态逆序对 可以求出初始逆序对,然后每次求出降低的逆序对个数即可。 对于每一个被删的数,它对逆序对个数的影响可分为两类: 在它前面,权值比它大,删除时间比它晚的点个数 在它后面,权值比它小,删除时间比它晚的点个数 这样就可以转化为三维偏序了。 【教程】简易CDQ分治教程&学习笔记 - mlystdcall - 博客园 (cnblogs.com) CDQ分治总结(CDQ,树状数组,归并排序) - Flash_Hu - 博客园 (cnblogs.com)

2023/4/28
articleCard.readMore

高斯消元笔记

消元法及高斯消元法思想 消元法是将方程组中的一方程的未知数用含有另一未知数的代数式表示,并将其带入到另一方程中,这就消去了一未知数,得到一解;或将方程组中的一方程倍乘某个常数加到另外一方程中去,也可达到消去一未知数的目的。消元法主要用于二元一次方程组的求解。 消元法理论的核心主要如下: 两方程互换,解不变; 一方程乘以非零数 $k$,解不变; 一方程乘以数 $k$ 加上另一方程,解不变。 解方程组: $$ \begin{cases} 2x_1+x_2-x_3=8 \ -3x_1-x_2+2x_3=-11 \ -2x_1+x_2+2x_3=-3 \end{cases} $$ 写成矩阵的形式为: $$ \left[\begin{matrix} 2 & 1 & -1 \ -3 & -1 & 2 \ -2 & 1 & 2 \end{matrix} \middle| \begin{matrix} 8 \ -11 \ -3 \end{matrix} \right] $$ 这种矩阵称为增广矩阵。所谓增广矩阵,即为方程组系数矩阵 $A$ 与常数列 $b$ 的并生成的新矩阵,即 $(A | b)$,增广矩阵行初等变换化为行最简形,即是利用了高斯消元法的思想理念,省略了变量而用变量的系数位置表示变量,增广矩阵中用竖线隔开了系数矩阵和常数列,代表了等于符号。 我们从上到下依次处理每一行,处理完第 $i$ 行后,让 $A_{ii}$ 非 $0$,而 $A_{ji}(j\gt i)$ 均为 $0$。过程如下。 $$ \left[\begin{matrix} 2 & 1 & -1 \ -3 & -1 & 2 \ -2 & 1 & 2 \end{matrix} \middle| \begin{matrix} 8 \ -11 \ -3 \end{matrix} \right] \ \Rightarrow \left[\begin{matrix} -3 & -1 & 2 \ & 1/3 & 1/3 \ & 5/3 & 2/3 \end{matrix} \middle| \begin{matrix} -11 \ 2/3 \ 13/3 \end{matrix} \right] \ \Rightarrow \left[\begin{matrix} -3 & -1 & 2 \ & 5/3 & 2/3 \ & & 1/5 \end{matrix} \middle| \begin{matrix} -11 \ 13/3 \ -1/5 \end{matrix} \right] $$ 其中最后一个增广矩阵的系数部分是上三角阵($0$ 用空白表示),而且主对角元 $a_{ii}$ 均非 $0$。该矩阵对应的方程组为: $$ \begin{cases} -3x_1-x_2+2x_3=-11 \ 5x_2/3+2x_3/3=13/3 \ x_3/5=-1/5 \end{cases} $$ 可得 $x_3=-1$,再代入前面两个方程可求解。 在消元部分中,假设正在处理第 $i$ 行,则首先需要找一个 $r\gt i$ 且绝对值最大的 $a_{ri}$,然后交换第 $r$ 行和第 $i$ 行。交换两个方程的位置不会对解产生影响,但可以减小计算误差。当 $A$ 可逆时,可以保证交换后 $a_{ii}$ 一定不等于 $0$。这种方法称为列主元法。 接下来进行所谓的“加减消元”。比如在上面的例子中的第一步,首先交换第一个和第二个方程的位置,然后用第一个方程 $(-3, 1, 2-11)$ 消去第二个方程 $(2, 1,-1, 8)$ 的第一列,方法是把第二个方程中的每个数都减去第一行对应元素的 $-2/3$ 倍。 一般情况下,如果要用第 $i$ 个方程来消去第 $k$ 个方程的第 $i$ 列,那么第 $k$ 行的所有元素 $A[k][j]$ 都应该减去 $A[i][j]$ 的 $A[k][i]/A[i][i]$ 倍。 下一个过程是回代。现在 $A$ 已经是一个上三角矩阵了,即第 $1,2,3…$ 行的最左边非 $0$ 元素分别在第 $1,2,3…$ 列。这样,最后一行实际上已经告诉我们 $x_n$ 的值了;接下来像前面说的那样不停地回代计算,最终会得到每个变量的唯一解。代码如下。 Luogu-P3389 【模板】高斯消元法 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; const int N = 106; const double EPS = 1e-7; int n; double a[N][N]; bool gauss() { int r; for (int i = 0; i < n; i++) { r = i; for (int j = i+1; j < n; j++) if (fabs(a[j][i]) > fabs(a[r][i])) r = j; if (fabs(a[r][i]) < EPS) return false; // 为 0 无法化简 if (r != i) swap(a[r], a[i]); for (int k = i+1; k < n; k++) { double f = a[k][i]/a[i][i]; for (int j = i; j <= n; j++) a[k][j] -= f*a[i][j]; } } // 回代 for (int i = n-1; i >= 0; i--) { for (int j = i+1; j < n; j++) a[i][n] -= a[j][n]*a[i][j]; a[i][n] /= a[i][i]; } return true; } int main() { cin >> n; for (int i = 0; i < n; i++) for (int j = 0; j <= n; j++) cin >> a[i][j]; if (gauss()) { for (int i = 0; i < n; i++) printf("%.2lf\n", a[i][n]); } else { puts("No Solution"); } return 0; } 高斯-乔丹?这个消元法不需要回代,代码更简单。 将高斯消元的上三角形式化为对角线形式,就是高斯-约旦消元。最终结果的系数部分的矩阵只有对角线上的可以为 $1$,其余都为 $0$。在代码上的区别就是将回代的部分转移到前面的循环而已。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bool gauss_jordan() { int r; for (int i = 0; i < n; i++) { r = i; for (int j = i+1; j < n; j++) if (fabs(a[j][i]) > fabs(a[r][i])) r = j; if (fabs(a[r][i]) < EPS) return false; // 为 0 无法化简 if (r != i) swap(a[r], a[i]); for (int k = 0; k < n; k++) { // 区别:高斯消元不处理小于 i 的行,因此最后需要回代 if (k == i) continue; double f = a[k][i]/a[i][i]; for (int j = i; j <= n; j++) a[k][j] -= f*a[i][j]; } } return true; } // 最后的输出部分改为: printf("%.2lf\n", a[i][n]/a[i][i]); Luogu-P2455 SDOI2006 线性方程组 高斯-约旦的一个缺点:相比于普通高斯,它更难判断无解和无穷多解。 具体地,我们用 $r$ 来记录当前行,如果主元为 $0$,那么 continue 到下一列,但 $r$ 不变;否则消元后令 $r\gets r+1$。 遍历完所有列后: 若 $r=n$,说明有唯一解; 若 $r\lt n$,说明第 $r+1\sim n$ 行系数矩阵全都是 $0$,若列向量全是 $0$,说明有无穷多解,否则无解。说人话就是,左边系数为 $0$ 而右边系数不为 $0$ 则无解,两边系数都为 $0$ 则有无穷多解。 注意首先判无解,再判多解。 为什么要额外用 $r$ 记录当前行,而不是向前面一样直接把 $i$ 看作当前行呢?想了很久终于明白了考虑这组数据: input: 4 1 1 1 1 0 0 0 2 4 6 0 0 1 1 2 0 0 4 8 12 output: 0 首先对于 $x_1$ 没有问题。到了 $x_2$,我们发现剩下三个方程 $x_2$ 的系数都是 $0$,说明没有唯一解。如果不用 $r$ 记录,我们处理第三个元时就会从第三行方程开始,而第二行的方程 $(0, 0, 2, 4, 6)$ 就会被忽略,答案就错了。 之前的普通高斯消元中,我们认为只有 $i$ 之后的式子是可用的,因为我们不用管具体是无解还是无数解,系数为 $0$ 直接判掉。 但这里,我们有可能会在系数为 $0$ 之后继续做下去。这就是受到消元顺序影响的原因。这就是为什么要用 $r$ 记录当前行。另一种方法见文后参考资料中 Rui_R 的题解。 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 #include <bits/stdc++.h> using namespace std; const int N = 106; const double EPS = 1e-7; int n; double a[N][N], ans[N]; int gauss_jordan() { int r = 0; for (int i = 0; i < n; i++) { // r 表示在枚举哪一行,i 表示列 int line = r; for (int j = r+1; j < n; j++) if (fabs(a[j][i]) > fabs(a[line][i])) line = j; if (r != line) swap(a[r], a[line]); if (fabs(a[r][i]) < EPS) continue; // 跳过当前列 for (int k = 0; k < n; k++) { if (k == r) continue; double f = a[k][i]/a[r][i]; for (int j = i; j <= n; j++) a[k][j] -= f*a[r][j]; } r++; } r--; if (r < n-1) { for (int i = r+1; i < n; i++) { if (a[i][n]) { // 0x != 0 无解 return -1; } } return 0; // 无穷多实数解 } for (int i = 0; i < n; i++) { ans[i] = a[i][n]/a[i][i]; if (fabs(ans[i]) < EPS) ans[i] = 0; } return 1; } int main() { cin >> n; for (int i = 0; i < n; i++) for (int j = 0; j <= n; j++) cin >> a[i][j]; int res = gauss_jordan(); if (res != 1) { printf("%d\n", res); } else { for (int i = 0; i < n; i++) { printf("x%d=%.2lf\n", i+1, ans[i]); } } return 0; } 异或方程组是指形如: $$ \begin{cases} a_{1,1}x_1 \oplus a_{1,2}x_2 \oplus \cdots \oplus a_{1,n}x_n &= b_1\ a_{2,1}x_1 \oplus a_{2,2}x_2 \oplus \cdots \oplus a_{2,n}x_n &= b_2\ \cdots &\cdots \ a_{m,1}x_1 \oplus a_{m,2}x_2 \oplus \cdots \oplus a_{m,n}x_n &= b_1 \end{cases} $$ 的方程组,且式中所有系数/常数(即 $a_{i,j}$ 与 $b_i$)均为 $0$ 或 $1$。 由于异或符合交换律与结合律,故可以按照高斯消元法逐步消元求解。值得注意的是,我们在消元的时候应使用异或消元而非加减消元,且不需要进行乘除改变系数(因为系数均为 $0$ 和 $1$)。 注意到异或方程组的增广矩阵是 $01$ 矩阵(矩阵中仅含有 $0$ 与 $1$),所以我们可以使用 bitset 进行优化,将时间复杂度降为 $O(\dfrac{n^2m}{\omega})$,其中 $n$ 为元的个数,$m$ 为方程条数,$\omega$ 一般为 $32$(与机器有关)。 消元时,当前方程的这个元的系数要为 $1$。否则就不用异或了~ Luogu-P2447 SDOI2010 外星千足虫 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int ans = 0, r; for (int i = 1; i <= n; i++) { r = i; while (r <= m && !a[r][i]) r++; if (r == m+1) { cout << "Cannot Determine\n"; return 0; } ans = max(ans, r); if (r != i) swap(a[r], a[i]); for (int j = 1; j <= m; j++) { if (j != i && a[j][i] == 1) { // 可以消元 a[j] ^= a[i]; } } } USACO09NOV Lights G [异或消元] Luogu-P2962 USACO09NOV Lights G 对于每一个点建立一个方程,若 $i$ 与 $j$ 之间有边,则将第 $i$ 个方程的第 $j$ 位系数和第 $j$ 个方程第 $i$ 位系数改为 $1$。初始化注意第 $i$ 个方程的第 $i$ 为系数一定是 $1$,方程等号的右边也是 $1$。 然后进行异或消元。因为有自由元,我们用 DFS 处理一下 $0$ 和 $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 #include <bits/stdc++.h> using namespace std; const int N = 45; int a[N][N], va[N]; int n, m, ans = 1E9; void dfs(int x, int tot) { if (tot > ans) return; if (!x) { ans = min(ans, tot); return; } if (a[x][x]) { va[x] = a[x][n+1]; for (int j = x+1; j <= n; j++) { if (a[x][j]) va[x] ^= va[j]; } if (va[x]) dfs(x-1, tot+1); else dfs(x-1, tot); } else { va[x] = 0; dfs(x-1, tot); va[x] = 1; dfs(x-1, tot+1); } } int main() { cin >> n >> m; for (int i = 1; i <= m; i++) { int x, y; cin >> x >> y; a[x][y] = a[y][x] = 1; } int r; for (int i = 1; i <= n; i++) a[i][n+1] = a[i][i] = 1; for (int i = 1; i <= n; i++) { r = i; while (!a[r][i] && r <= n) r++; if (r == n+1) continue; if (r != i) swap(a[r], a[i]); for (int j = 1; j <= n; j++) { if (j != i && a[j][i]) for (int k = 1; k <= n+1; k++) a[j][k] ^= a[i][k]; } } dfs(n, 0); cout << ans << endl; return 0; } Luogu-P2973 USACO10HOL Driving Out the Piggies G 题意:一个无向图,节点 $1$ 有一个炸弹,在每个单位时间内,有 $p/q$ 的概率在这个节点炸掉,有 $1-p/q$ 的概率随机选择一条出去的路到其他的节点上。问最终炸弹在每个节点上爆炸的概率。 进入一个点的次数是无限的,因此求概率似乎比较难。 对于这种题目,我们一般考虑 使用方程来推出相邻两个状态之间的关系,从而解决无数的路径 。 我们设第 $i$ 个点的期望经过次数为 $f_i$,那么第 $i$ 个点的爆炸概率为 $f_i \times \frac{P}{Q}$。问题在于求 $f_i$。我们用 $deg_i$ 表示点 $i$ 的度数。 $$ f_u=\begin{cases}1& i=1 \\sum_{(u,v)\in E} (1-\frac{P}{Q})\times f_v \times \frac{1}{deg_v} & \text{otherwise}\end{cases} $$ 移项后就是: $$ -f_u+\sum_{(u,v)\in E} (1-\frac{P}{Q})\times f_v \times \frac{1}{deg_v}=0 $$ 然后对于这个方程组进行高斯消元即可。 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 #include <bits/stdc++.h> using namespace std; const int N = 305, M = 50000; int n, m; double p, q; int mp[N][N], in[N]; double a[N][N]; void gauss_jordan() { int r; for (int i = 1; i <= n; i++) { r = i; for (int j = i+1; j <= n; j++) if (fabs(a[j][i]) > fabs(a[r][i])) r = j; if (r != i) swap(a[r], a[i]); for (int j = 1; j <= n; j++) { if (j != i) { double t = a[j][i]/a[i][i]; for (int k = i; k <= n+1; k++) { a[j][k] -= t*a[i][k]; } } } } } int main() { cin >> n >> m >> p >> q; double pdq = p/q; for (int i = 1; i <= m; i++) { int u, v; cin >> u >> v; mp[u][v] = mp[v][u] = 1; in[u]++; in[v]++; } for (int i = 1; i <= n; i++) { a[i][i] = 1; for (int j = 1; j <= n; j++) { if (!mp[i][j]) continue; a[i][j] = -(1.0-pdq)/in[j]; } } a[1][n+1] = 1; gauss_jordan(); for (int i = 1; i <= n; i++) { double ans = a[i][n+1]/a[i][i]; printf("%.9lf\n", ans*pdq); } return 0; } 《算法竞赛入门经典-训练指南》 【数学】高斯-约旦消元法 - mango09 OI Wiki 数论小白都能看懂的线性方程组及其解法 - ShineEternal的笔记小屋 题解 P2455 【SDOI2006线性方程组】Rui_R 扶苏的bitset浅谈

2023/4/28
articleCard.readMore

二分图笔记

定义 在图论中,二分图(bipartite graph)是一类特殊的图,又称为二部图、偶图、双分图。二分图的顶点可以分成两个互斥的独立集 $U$ 和 $V$ 的图,使得所有边都是连结一个 $U$ 中的点和一个 $V$ 中的点。 给定一个二分图 $G$,在 $G$ 的一个子图 $M$ 中,$M$ 的边集中的任意两条边都没有共同的端点,则称 $M$ 是一个匹配。 最小点覆盖:选最少的点,满足每条边至少有一个端点被选。 交错路始于非匹配点且由匹配边与非匹配边交错而成。 增广路是始于非匹配点且终于非匹配点的交错路。 二分图中不存在奇环 因为每一条边都是从一个集合走到另一个集合,只有走偶数次才可能回到同一个集合。 König 定理:一个图是二分图当且仅当它的最小顶点覆盖的顶点数等于最大匹配的边数 首先,最小点集覆盖一定 >= 最大匹配,因为假设最大匹配为 $n$,那么我们就得到了 $n$ 条互不相邻的边,光覆盖这些边就要用到 $n$ 个点。现在我们来思考为什么最小点击覆盖一定 <= 最大匹配。任何一种 $n$ 个点的最小点击覆盖,一定可以转化成一个 $n$ 的最大匹配。因为最小点集覆盖中的每个点都能找到至少一条只有一个端点在点集中的边(如果找不到则说明该点所有的边的另外一个端点都被覆盖,所以该点则没必要被覆盖,和它在最小点集覆盖中相矛盾),只要每个端点都选择一个这样的边,就必然能转化为一个匹配数与点集覆盖的点数相等的匹配方案。所以最大匹配至少为最小点集覆盖数,即最小点击覆盖一定 <= 最大匹配。综上,二者相等。 染色法:用 $1,2$ 两种颜色标记图中的节点,与一个节点相邻的所有节点的颜色必须和它不同,若标记过程中出现冲突,说明图中存在奇环。使用 DFS 实现。$O(N+M)$。 CF687A NP-Hard Problem 二分图判定裸题。 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 #include <bits/stdc++.h> using namespace std; const int N = 100005; int n, m; vector<int> e[N]; int c[N], cnt[3]; bool flag = true; void dfs(int u, int col) { c[u] = col; for (int i = 0; i < e[u].size(); i++) { int v = e[u][i]; if (c[v] == 0) { dfs(v, 3-col); // 1 <=> 2 } else if (c[v] == col) { // 出现冲突 flag = 0; } } } int main() { cin >> n >> m; for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } for (int i = 1; i <= n; i++) { flag = true; if (!c[i]) { dfs(i, 1); } if (!flag) { cout << "-1\n"; return 0; } } vector<int> ans; for (int i = 1; i <= n; i++) if (c[i] == 1) ans.push_back(i); cout << ans.size() << endl; for (int i = 0; i < ans.size(); i++) cout << ans[i] << ' '; cout << endl; ans.clear(); for (int i = 1; i <= n; i++) if (c[i] == 2) ans.push_back(i); cout << ans.size() << endl; for (int i = 0; i < ans.size(); i++) cout << ans[i] << ' '; cout << endl; return 0; } 运用了贪心的思想。 整体思想:将匹配边集 $M$ 置为空,寻找一条增广路径 $P$,$P$ 上的边取反得到一个更大的匹配 $M’$ 重复上一步直到找不到增广路径为止。$O(NM)$。 具体步骤: 依次考虑每个左部未匹配点,寻找一个右部点与之匹配。一个右部点能与之匹配,需要满足以下两个条件之一: 该点是未匹配点:此时直接把两个点进行匹配。(mat[i] == 0) 从与该节点匹配的左部点出发,可以找到另一个右部点与之匹配。此时递归进入该左部点,为其寻找匹配的右部点。(dfs(mat[v]) == 1) 利用访问标记(use[])避免重复搜索。记得每次 dfs 之前要清空 use 数组! UOJ #78. 二分图最大匹配 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 #include <bits/stdc++.h> using namespace std; const int N = 1005, V = 500005; int n, m, e, ans, outp[N]; int to[V], head[N], nxt[V], tot; bool use[N]; int mat[N]; void add(int x, int y) { to[++tot] = y; nxt[tot] = head[x]; head[x] = tot; } bool dfs(int u) { for (int i = head[u]; i; i = nxt[i]) { int v = to[i]; if (use[v] == 0) { use[v] = 1; if (mat[v] == 0 || dfs(mat[v])) { mat[v] = u; return true; } } } return false; } int main() { cin >> n >> m >> e; for (int i = 0; i < e; i++) { int u, v; cin >> u >> v; add(u, v); } for (int i = 1; i <= n; i++) { memset(use, 0, sizeof use); // 记得清空 use if (dfs(i)) ans++; // 从左部点 i 出发找增广路 } cout << ans << endl; for (int i = 1; i <= m; i++) { outp[mat[i]] = i; } for (int i = 1; i <= n; i++) { cout << outp[i] << ' '; } return 0; } 好复杂,不会。 二分图匹配 - 菜MKのblog - 洛谷博客 二分图 - Wikipedia 二分图 - OI Wiki 简单理解增广路与匈牙利算法 - 朴楞蛾子 理解增广路 匈牙利算法(二分图) 证明 König 定理 图论 - 李煜东.pptx

2023/4/15
articleCard.readMore

ABC133F Colorful Tree

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/4/14
articleCard.readMore

欧拉函数笔记

定义 欧拉函数 $\varphi(n)$ 表示小于等于 $n$,且与 $n$ 互质的正整数的个数。 如何求 $\varphi(n)$? 比如 $varphi(12)$ 把 $12$ 质因数分解,$12=2^2*3$,其实就是得到了 $2$ 和 $3$ 两个互异的质因子。 然后把 $2$ 的倍数和 $3$ 的倍数都删掉。 $2$ 的倍数:$2,4,6,8,10,12$ $3$ 的倍数:$3,6,9,12$ 但是是 $6$ 和 $12$ 重复减了。所以还要把既是 $2$ 的倍数又是 $3$ 的倍数的数加回来。所以这样写:$12 - 12/2 - 12/3 + 12/(2*3)$。运用了容斥原理。 欧拉函数是积性函数。 积性是什么意思呢?如果有 $\gcd(a, b) = 1$,那么 $\varphi(a \times b) = \varphi(a) \times \varphi(b)$。 特别地,当 $n$ 是奇数时 $\varphi(2n) = \varphi(n)$。 $n = \sum_{d \mid n}{\varphi(d)}$。 证明: 也可以这样考虑:如果 $\gcd(k, n) = d$,那么 $\gcd(\dfrac{k}{d},\dfrac{n}{d}) = 1, ( k < n )$。 如果我们设 $f(x)$ 表示 $\gcd(k, n) = x$ 的数的个数,那么 $n = \sum_{i = 1}^n{f(i)}$。 根据上面的证明,我们发现,$f(x) = \varphi(\dfrac{n}{x})$,从而 $n = \sum_{d \mid n}\varphi(\dfrac{n}{d})$。注意到约数 $d$ 和 $\dfrac{n}{d}$ 具有对称性,所以上式化为 $n = \sum_{d \mid n}\varphi(d)$。 若 $n = p^k$,其中 $p$ 是质数,那么 $\varphi(n) = p^k - p^{k - 1}$。(根据定义可知) 由唯一分解定理,设 $n = \prod_{i=1}^{s}p_i^{k_i}$,其中 $p_i$ 是质数,有 $\varphi(n) = n \times \prod_{i = 1}^s{\dfrac{p_i - 1}{p_i}}$。 证明: 引理:设 $p$ 为任意质数,那么 $\varphi(p^k)=p^{k-1}\times(p-1)$。 证明:显然对于从 1 到 $p^k$ 的所有数中,除了 $p^{k-1}$ 个 $p$ 的倍数以外其它数都与 $p^k$ 互素,故 $\varphi(p^k)=p^k-p^{k-1}=p^{k-1}\times(p-1)$,证毕。 接下来我们证明 $\varphi(n) = n \times \prod_{i = 1}^s{\dfrac{p_i - 1}{p_i}}$。由唯一分解定理与 $\varphi(x)$ 函数的积性 $$ \begin{aligned} \varphi(n) &= \prod_{i=1}^{s} \varphi(p_i^{k_i}) \\ &= \prod_{i=1}^{s} (p_i-1)\times {p_i}^{k_i-1} \\ &=\prod_{i=1}^{s} {p_i}^{k_i} \times(1 - \frac{1}{p_i}) \\ &=n~ \prod_{i=1}^{s} (1- \frac{1}{p_i}) &\square \end{aligned} $$ $\varphi(a*b) = \varphi(a) * \varphi(b) * \frac{d}{\varphi(d)}$ ,其中 $d = \gcd(a, b)$ 1 2 3 4 5 6 7 8 9 10 int euler_phi(int n) { int ans = n; for (int i = 2; i * i <= n; i++) if (n % i == 0) { ans = ans / i * (i - 1); while (n % i == 0) n /= i; } if (n > 1) ans = ans / n * (n - 1); return ans; } 在线性筛中,每一个合数都被最小的质因子筛掉。设 $p_1$ 是 $n$ 的最小质因子,$n’ = \frac{n}{p_1}$,那么 $n$ 通过 $n’ \times p_1$ 筛掉。 对 $n’ \mod p_1$ 分类讨论。 $n’ \mod p_1 = 0$,那么 $n’$ 包含了 $n$ 的所有质因子。 $$ \begin{aligned} \varphi(n) &= n \times \prod_{i = 1}^s{\dfrac{p_i - 1}{p_i}} \\ &= p_1 \times n’ \times \prod_{i = 1}^s{\dfrac{p_i - 1}{p_i}} \\ &= p_1 \times \varphi(n’) \end{aligned} $$ $n’ \mod p_1 \ne 0$,此时 $n’$ 与 $p_1$ 互质,根据欧拉函数的性质,我们有: $$ \begin{aligned} \varphi(n) &= \varphi(p_1) \times \varphi(n’) \\ &= (p_1-1) \times \varphi(n’) \end{aligned} $$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void sieve() { memset(isprime, true, sizeof isprime); tot = 0; isprime[1] = 0; phi[1] = 1; for (int i = 2; i <= n; i++) { if (isprime[i]) { prime[++tot] = i; phi[i] = i-1; } for (int j = 1; j <= tot && prime[j]*i <= n; j++) { isprime[prime[j]*i] = 0; if (i%prime[j] == 0) { phi[i*prime[j]] = phi[i]*prime[j]; break; } phi[i*prime[j]] = phi[i]*phi[prime[j]]; } } } 欧拉函数 - OI Wiki

2023/4/1
articleCard.readMore

欧拉回路笔记

定义 欧拉回路:通过图中每条边恰好一次的回路 欧拉通路(欧拉路径):通过图中每条边恰好一次的通路 欧拉图:具有欧拉回路的图 半欧拉图:具有欧拉通路但不具有欧拉回路的图 无向图是欧拉图当且仅当: 非零度顶点是连通的 顶点的度数都是偶数 无向图是半欧拉图当且仅当: 非零度顶点是连通的 恰有 0 或 2 个奇度顶点 有向图是欧拉图当且仅当: 非零度顶点是强连通的 每个顶点的入度和出度相等 有向图是半欧拉图当且仅当: 非零度顶点是弱连通的 至多一个顶点的出度与入度之差为 1 至多一个顶点的入度与出度之差为 1 其他顶点的入度和出度相等 弱连通:将所有有向边替换为无向边后,整张图连通。 Hierholzer 算法的具体步骤:遍历当前节点的所有出边,并 DFS 访问相邻顶点,将经过的边删掉。遍历完所有出边后,将 $u$ 加入栈中。最后把栈中的顶点反过来,再输出,就是欧拉回路。 如果要求字典序最小,只需在一开始对每个点的所有出边从小到大排序。这样一来,欧拉回路上从左往右看,每个点的后继都取到了理论最小值。 对于无向图和有向图的欧拉路径,必须从奇点或唯一的出度比入度大 1 的点开始 dfs。 Luogu-P7771 【模板】欧拉路径 实现时,要注意一个小细节:用 hd[x] 数组记录节点 $x$ 目前删到了哪条边,每次走过一条边时要 hd[x]++,然后下次到达这个点时再调用这个值。否则的话,每次到了这个点都要从 0 开始判断这些边是否被删除掉,会超时。 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 #include <bits/stdc++.h> using namespace std; const int N = 2e5+5; int n, m, S, top, sta[N], in[N], hd[N]; vector<int> e[N]; void dfs(int u) { for (int &i = hd[u]; i < e[u].size(); ) { // 细节 dfs(e[u][i++]); } sta[++top] = u; } int main() { cin >> n >> m; for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; e[u].push_back(v); in[v]++; } for (int i = 1; i <= n; i++) { sort(e[i].begin(), e[i].end()); if (abs((int)e[i].size()-in[i]) > 1) { puts("No"); return 0; } if (e[i].size() > in[i]) { if (S) { puts("No"); return 0; } else S = i; } } dfs(S ? S : 1); if (top != m+1) puts("No"); else { reverse(sta+1, sta+top+1); for (int i = 1; i <= top; i++) { cout << sta[i] << " "; } } return 0; } Luogu-P1341 无序字母对:把每个字符当做一个点,把输入的字符串看做两个字符间可以连一条边,最后要求的字符串就是找一笔画,即欧拉回路(路径)。要判断图是否连通。 Luogu-P1127 词链:与上一道题类似 ABC286G Unique Walk:对于非关键边,走多少次都无所谓,可以把非关键边形成的连通块缩成一个点,然后再判欧拉路 初级图论 - qAlex_Weiq - 博客园 欧拉图 - OI Wiki

2023/3/25
articleCard.readMore

割点和桥笔记

定义 若对于无向连通图的一个点 $x$,从图中删去这个点和与这个点相连的所有边后,图不再是连通图,则 $x$ 为这个图的割点。 若对于无向连通图的一条边 $e$,从图中删去这条边后,图不再是连通图,则 $e$ 为这个图的割边(桥)。 无向图的搜索树 从任意一个点出发进行 DFS,每个点只能访问一次,所有被访问过的结点和边构成一棵搜索树。 然后就可以将图上的边分为两类,树边和返祖边,返祖边连接了一个点和它的一个祖先。 $dfn[x]$ 表示在 DFS 的过程中,$x$ 第一次被访问的顺序。 $low[x]$ 表示 $x$ 和 $x$ 的子树中所有点的时间戳 和 从 $x$ 的子树中的点通过仅一条返祖边可以达到的点的时间戳 的最小值。 更新 $low$ 的方法: 如果 $v$ 是 $u$ 的儿子:$low[u] = min(low[u], low[v])$ 否则:$low[u] = min(low[u], dfn[v])$ 对于某个点 $u$,如果它的儿子中存在一个点 $v$,使得 $low[v] \ge dfn[u]$,即不能回到祖先,那么 $u$ 就是割点。 对于搜索树的根节点就比较特殊,如果它在搜索树中只有一个儿子,是不能成为割点的,需要特判。 树的叶子节点由于没有儿子,也不能成为割点。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void tarjan(int u, int father) { int child = 0; vis[u] = 1; low[u] = dfn[u] = ++inde; for (int i = 0; i < g[u].size(); i++) { int v = g[u][i]; if (!vis[v]) { child++; tarjan(v, u); low[u] = min(low[u], low[v]); if (father != u && low[v] >= dfn[u] && !flag[u]) { flag[u] = 1; res++; } } else if (v != father) { low[u] = min(low[u], dfn[v]); } } if (u == father && child >= 2 && !flag[u]) { // 特判根节点 flag[u] = 1; res++; } } 与割点同理,只需要修改成:$low[v] \gt dfn[u]$ 即可,不需要考虑是否为根节点。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void tarjan(int u, int fa) { father[u] = fa; low[u] = dfn[u] = ++inde; for (int i = 0; i < G[u].size(); i++) { int v = G[u][i]; if (!dfn[v]) { tarjan(v, u); low[u] = min(low[u], low[v]); if (low[v] > dfn[u]) { isbridge[v] = true; ++cnt_bridge; } } else if (dfn[v] < dfn[u] && v != fa) { low[u] = min(low[u], dfn[v]); } } } [算法笔记] 割点与割边 - installb 割点和桥 - OI Wiki

2023/3/19
articleCard.readMore

树链剖分笔记

树链剖分(本文仅介绍 重链剖分(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$ 之间的信息,重复以下步骤: 如果 $u$,$v$ 不在同一条链上,设 $u$ 所在链链顶深度较大,则查出 $u$ 的链顶到 $u$ 的信息(依据同一条重链上 DFN 序是连续的),然后将 $u$ 跳到 $u$ 所在链链顶的父亲 如果 $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 序是连续的,我们可以记录所在子树连续区间末端的结点,就可以把子树转化为连续的一段区间。 本文中的理论内容基本来自 树链剖分 - OI Wiki。 树链剖分学习笔记 - Menci 🙇‍

2023/2/25
articleCard.readMore

CSP-J/S2022 题解与反思

学校 OI 停(tui yi)了,周末有空的时候补补。 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; } 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; } 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; } 话说有没有发现今年 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; } 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; } 唯一做出来的一道 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; } 直接放弃。 纯骗分的。还是不会做。 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; }

2022/11/5
articleCard.readMore

ARC101B Median of Medians - 中位数

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; } https://img.atcoder.jp/arc101/editorial.pdf https://www.luogu.com.cn/blog/DZN2004/atcoder-shang-di-yi-suo-ti https://zuytong.blog.luogu.org/post-20221012-d# @ 2022/10/15 SM 模拟赛。

2022/10/23
articleCard.readMore

CF-342E Xenia and Tree - 根号分治

CF342E Xenia and Tree 给定一棵 $n$ 个节点的树,初始时 1 号节点为红色,其余为蓝色。 要求支持如下操作: 将一个节点变为红色。 询问节点 $u$ 到最近红色节点的距离。 共 $q$ 次操作。 $1 \le n, q \le 10 ^5$ 首先我们有两种暴力思路: 每次将一个点变为红色,就从那个点开始 BFS,更新它周边结点的最小值,直到无法更新。 每次询问,都和之前的红色点求 LCA,计算出距离,再取最小值。 这两种做法都过不了。但我们可以将它们结合起来,这就是根号分治(a.k.a. 操作分块)。 我们把操作序列以 $\sqrt m$ 为块长分块,对于一个询问,有两种情况: 在同一块内且在询问之前的修改,可以暴力 LCA 求距离。 对于之前块的修改,可以在处理完那个块之后,从块中修改的红点开始多源 BFS 更新每个点的答案。 最后答案便是两种情况取最小值。 大概也许是 $O((n+m)\sqrt 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 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 #include <bits/stdc++.h> using namespace std; const int MAXN = 100005; const int LOGN = 17; int n, m; int f[MAXN][LOGN], depth[MAXN]; int tmpdis[MAXN]; vector<int> g[MAXN]; void dfs(int cur, int father) { depth[cur] = depth[father] +1; f[cur][0] = father; for (int i = 1; i < LOGN; i++) f[cur][i] = f[f[cur][i-1]][i-1]; for (int i = 0; i < g[cur].size(); i++) { if (g[cur][i] == father) continue; dfs(g[cur][i], cur); } } 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 dist(int u, int v) { int l = lca(u, v); return depth[u]+depth[v] - 2*depth[l]; } int main() { scanf("%d%d", &n, &m); for (int i = 0; i < n-1; i++) { int u, v; scanf("%d%d", &u, &v); g[u].push_back(v); g[v].push_back(u); } dfs(1, 1); int b = sqrt(m); vector<int> buf; memset(tmpdis, 0x3f, sizeof tmpdis); buf.push_back(1); for (int i = 0; i < m; i++) { int type, v; scanf("%d%d", &type, &v); if (type == 1) { buf.push_back(v); } else { // 之前块的答案记在 tmpdis 中 int ans = tmpdis[v]; for (int i = 0; i < buf.size(); i++) ans = min(ans, dist(v, buf[i])); // 当前块直接暴力 LCA printf("%d\n", ans); } if (i%b == 0) { queue<int> q; for (int i = 0; i < buf.size(); i++) { q.push(buf[i]); tmpdis[buf[i]] = 0; } buf.clear(); // BFS 记录答案 while (!q.empty()) { int f = q.front(); q.pop(); for (int i = 0; i < g[f].size(); i++) { if (tmpdis[g[f][i]] > tmpdis[f]+1) { tmpdis[g[f][i]] = tmpdis[f]+1; q.push(g[f][i]); } } } } } return 0; } 参考:[CF342E] Xenia and Tree - 分块,ST表,LCA - Mollnn - 博客园 @ 2022/10/6 SM 模拟赛。

2022/10/6
articleCard.readMore

最短路笔记

又是一年 CSP 复赛,已经一年没写过最短路了,赶紧复习一下。 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 算法解决的是一般情况下的单源最短路径问题,边的权重可以为负值。它可以判断是否存在一个从源节点可以到达的负环。 先解释一下松弛操作。对于边 $(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 也可以用于判断 $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 算法要求所有边的权重都为非负值,运用贪心策略。我们把结点分为两个集合:已确定最短路长度的点集 $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 OI Wiki CC BY-SA 4.0 《算法导论》 《算法竞赛入门经典》

2022/10/1
articleCard.readMore

CSP-J 2022 游记

CSP-J1/S1 这次和初一的时候一样,在自己学校考试,舒服。 感觉题目偏简单,但是出了很多奇怪的题目。。。(网上的 dalao 都说有一堆错误? 估分 J 组 90,S 组 68,应该也许能过吧。 结果 J 组 92,S 组 65.5。 GD 出分数线一如既往地咕咕咕咕咕。 本来 S 组应该才勉强压线(ZJ 64),但是今年 GD 貌似加了很多机位,分数线才 55。于是初三的我第一次进了 S 组复赛。。。 Day -? 本来说要去东莞考的,后来又改到石中了,有点失望。 周五。全市封校。:) 上次被疫情搞得回不了家还是初一下学期的时候。 早上五点四十起床,坐大巴去石中。 J 组,前两题大概花了一小时。T3 和 2020-T3 如出一辙,考前一晚看过,但是没看懂。于是考场上花了很久思考,最后想到一种实现方式,就没有思考过时间复杂度直接开写,结果大样例超时,一共花了一小时。最后花了十几分钟乱写 T4,总体自我感觉良好。 中午饭还算挺香,饭后在草丛里捡了个篮球,打了十几分钟,一个过路老师阴沉地说:“中午不准打球。”(鬼知道我在石中听到这句话多少次了。。。) S 组,同桌是 czz 大神。慢慢地看 T1,发现还是没有头绪,打了个暴力。然后看到 T2,感觉有点希望,然后发现好水,赶紧做了,大喜。再看 T3,直接放弃。T4 再打了个暴力。这时候就到了六点,静坐了半小时,出考场。 考完之后感觉题简单了,运气好也许能冲 1=? 洛谷自测 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 结束了,三年的初中信息组生涯也结束了。诶,不知道该说什么。

2022/9/25
articleCard.readMore

CF-1385E Directing Edges

CF-1385E Directing Edges 给定一个由有向边与无向边组成的图,现在需要你把所有的无向边变成有向边,使得形成的图中没有环。 如果可以做到请输出该图,否则直接输出"NO"。 我们先只连接有向边,然后做一遍拓扑排序,如果失败了,就说明有环,输出 “NO”。 然后处理剩下的无向边。对于无向边 $(u, v)$,如果 $u$ 的拓扑序小于 $v$,那么令这条边的方向是 $u\rightarrow v$。否则,方向就是 $v\rightarrow u$。因为这条边是从拓扑序小的点指向拓扑序大的点,所以必然不会形成环。 RECORD 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 #include <algorithm> #include <cstdio> #include <cstring> #include <iostream> #include <vector> using namespace std; const int MAXN = 200005; int n, m; vector<int> G[MAXN]; int c[MAXN], topo[MAXN], id[MAXN], t, bn, x[MAXN], y[MAXN]; bool dfs(int u) { c[u] = -1; for (auto v : G[u]) { if (c[v] < 0) return false; else if (c[v] == 0 && !dfs(v)) return false; } c[u] = 1; topo[--t] = u; id[u] = t; return true; } bool toposort() { t = n; memset(c, 0, sizeof(c)); memset(id, 0, sizeof(id)); memset(topo, 0, sizeof(topo)); for (int i = 1; i <= n; i++) if (!c[i]) if (!dfs(i)) return false; return true; } int main() { ios::sync_with_stdio(false); int t; cin >> t; while (t--) { cin >> n >> m; for (int i = 1; i <= n; i++) G[i].clear(); bn = 0; for (int i = 0; i < m; i++) { int ti, tx, ty; cin >> ti >> tx >> ty; if (ti == 0) { // 无向 x[++bn] = tx; y[bn] = ty; } else { G[tx].push_back(ty); } } if (!toposort()) { cout << "NO\n"; continue; } cout << "YES\n"; for (int i = 1; i <= bn; i++) { if (id[x[i]] <= id[y[i]]) cout << x[i] << " " << y[i] << endl; else cout << y[i] << " " << x[i] << endl; } for (int i = 1; i <= n; i++) { for (auto j : G[i]) { cout << i << " " << j << endl; } } } return 0; }

2022/8/26
articleCard.readMore

拓扑排序笔记

引入 给定一张有向无环图(DAG, Directed Acyclic Graph),对其顶点进行排序,使得对于每条从 $u$ 到 $v$ 的有向边 $(u, v)$,$u$ 在排序中都在 $v$ 的前面。这种排序就称为拓扑排序(Topological sorting)。 当且仅当图中没有定向环时(即有向无环图),才有可能进行拓扑排序。如果排序失败,就说明该有向图存在环,不是 DAG。 任何有向无环图至少有一个拓扑排序。 举例:在某校的选课系统中,存在这样的规则:每门课可能有若干门先修课,如果要修读某一门课,则必须要先修读此课程所要求的先修课后才能修读。假设一个学生同时只能修读一门课程,那么,被选课系统允许的他修完他需要所有课程的顺序是一个拓扑序。 在这个例子中,每一门课程对应有向图中的一个顶点,每一个先修关系对应一条有向边(从先修课指向需要先修课的课)。 Kahn 算法 初始状态下,集合 $S$ 装着所有入度为 $0$ 的点,$L$ 是一个空列表。 每次从 $S$ 中任意取出一个点 $u$ 放入 $L$, 然后将 $u$ 的所有边 $(u, v_1), (u, v_2), (u, v_3) \cdots$ 删除。对于边 $(u, v)$,若将该边删除后点 $v$ 的入度变为 $0$,则将 $v$ 放入 $S$ 中。 不断重复以上过程,直到集合 $S$ 为空。检查图中是否存在任何边,如果有,那么这个图一定有环路,否则返回 $L$,$L$ 中顶点的顺序就是拓扑排序的结果。 基本上就是 BFS 的框架。 代码实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int n, m; vector<int> G[MAXN]; int in[MAXN]; // 存储每个结点的入度 bool toposort() { vector<int> L; queue<int> S; for (int i = 1; i <= n; i++) if (in[i] == 0) S.push(i); while (!S.empty()) { int u = S.front(); S.pop(); L.push_back(u); for (auto v : G[u]) { if (--in[v] == 0) { S.push(v); } } } if (L.size() == n) { for (auto i : L) cout << i << ' '; return true; } else { return false; } } 借助 DFS 完成拓扑排序:在访问完一个结点之后把它加到当前拓扑序的首部。 代码实现: 当 c[u] == 0 时,表示从来没有被访问过(从来没有调用过 dfs(u));c[u] == 1 时,表示已经访问过,而且还递归访问过它的所有子孙(即调用过 dfs(u) 且已返回);c[u] == -1 时表示正在访问(即递归调用 dfs(u) 正在栈帧中,尚未返回)。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int n, m; vector<int> G[MAXN]; int c[MAXN], topo[MAXN], t; bool dfs(int u) { c[u] = -1; for (auto v : G[u]) { if (c[v] < 0) return false; else if (c[v] == 0 && !dfs(v)) return false; } c[u] = 1; topo[--t] = u; return true; } bool toposort() { t = n; memset(c, 0, sizeof(c)); for (int i = 1; i <= n; i++) if (!c[i]) if (!dfs(i)) return false; return true; } UVA10305 给任务排序 Ordering Tasks 例题 CF1385E Directing Edges Luogu-P1137 旅行计划 拓扑排序+DP OI Wiki CC BY-SA 4.0 《算法竞赛入门经典》

2022/8/26
articleCard.readMore

位运算笔记

基本概念 比特(bit,亦称二进制位)是指 1 位二进制的数码(0 或 1),是计算机中信息的最小单位。 字节(byte):一个字节由 8 位组成。 熟练地运用位运算,可以提高我们程序的时空效率。 下面以 32 位二进制数,即 C++ 中的 int 和 unsigned int 类型为例。 简单介绍一下: 原码:最高位为符号位,正数为 $0$,负数为 $1$,其余所有位为十进制数的绝对值。 优点:对人类而言最直观。 缺点:无法将减法转换成加法运算。如:$1-1=1+(-1)=0001+1001=1010=-2$;$0$ 有两种表示方法 $0000$ 和 $1000$。 反码:最高位为符号位,正数为 $0$,负数为 $1$。正数的反码等于本身,负数的反码除符号位外,各位取反。 优点:解决了减法运算的问题。$1-1=1+(-1)=0001+1110=1111=0$ 缺点:$0$ 有两种表示方法 $0000$ 和 $1111$;减法算法规则较复杂,需要额外判断溢出。 32 位无符号整数 unsigned int: 直接把这 32 位编码 $C$ 看作 32 位二进制数 $N$。 32 位有符号整数 int: 以最高位作为符号位,$0$ 表示非负数,$1$ 表示负数。 对于最高位为 $0$ 的每种编码 $C$,直接看做 32 位二进制数 $S$。 同时,定义该编码按位取反后得到的编码 ~C 表示的数值为 $-1-S$。 32 位补码表示 unsigned int int $000000\cdots 000000$ $0$ $0$ $011111\cdots 111111$ $2147483647$ $2147483647$ $100000\cdots 000000$ $2147483648$ $-2147483648$ $111111\cdots 111111$ $4294967295$ $-1$ 可以发现,在补码下,每个数值都有唯一的表示方式,并且任意两个数值做加减法运算,都等价于在 32 位补码下做最高位不进位的二进制加减法运算。发生上/下溢出时,32 位无符号整数和有符号整数都相当于自动对 $2^{32}$ 取模(回绕)。 这也解释了有符号整数算术上溢时会出现负数的现象。我们来对 int 的溢出做一个实验: 1 2 3 4 5 6 7 8 void print(int t) { cout << bitset<32>(t) << " " << t << endl; } int main() {   int t = 2147483647;   print(t);   print(t + 1);   print((t + 1) * 2);   return 0; } 输出: 1 2 3 01111111111111111111111111111111 2147483647 10000000000000000000000000000000 -2147483648 00000000000000000000000000000000 0 (t+1)*2 的结果本应是 $1\underbrace{00000\cdots 000000}{32 个 0}$,依据最高位不进位原则,结果变成了 $\underbrace{00000\cdots 000000}{32 个 0} = 0$。 补码也被称为“二补数”(Two’s complement)。反码也叫“一补数”(Ones’ complement),直接把 $C$(正数)的每一位取反表示负 $C$。补码与反码在负数表示中,绝对值相差 $1$。例如在上面的表格中,第一、四行是一对反码,第二、三行是一对反码。作为整数编码,补码比反码有很多优势。除了上面提到的“自然溢出取模”之外,补码重点解决了 $0$ 的编码唯一性问题,能比反码多表示一个数。同时减少特殊判断,在电路设计中简单高效。 形式 加数 加数 和 32 位补码 $111\cdots 111$ $000\cdots 001$ $(1)_{溢出}000\cdots 000$ int $-1$ $1$ $0$ unsigned int $4294967295$ $1$ $0(\mod 2^{32})$ “反码加一”只是补码所具有的一个性质,不能被定义成补码。负数的补码,是能够和其相反数相加通过溢出从而使计算机内计算结果变为 $0$ 的二进制码。这是补码设计的初衷,具体目标就是让 $1+(-1)= 0$,这利用原码是无法得到的。 所以对于一个 $n$ 位的负数 $-X$,有如下关系: $$ X_补 + (-X)_补 = 1\underbrace{00\cdots 0}_n = 2^n $$ 所以 $-X$ 的补码应该是 $2^n-X$ 的二进制编码。 形式 加数 加数 和 32 位补码 $011\cdots 111$ $000\cdots 001$ $100\cdots 000$ int $2147483647$ $1$ $-2147483648$ unsigned int $2147483647$ $1$ $2147483648$ 因为用二进制表示一个 int 需要写出 32 位,比较繁琐。而用十进制表示,又不容易明显地体现出补码的每一位,所以在程序设计中,常用十六进制来表示一个常数,这样只需要书写 8 个字符,每个字符($0\sim 9, A\sim F$)代表补码下的 4 个二进制位。C++ 的十六进制常数以 0x 开头,0x 本身只是声明了进制,0x 后面的部分对应具体的十六进制数值。例如: 32 位补码 int(十进制) int(十六进制) $000000\cdots 000000$ $0$ $0x0$ $011111\cdots 111111$ $2147483647$ $0x7F\thinspace FF\thinspace FF\thinspace FF$ $00111111 重复4次$ $1061109567$ $0x3F\thinspace 3F\thinspace 3F\thinspace 3F$ $111111\cdots 111111$ $-1$ $0xFF\thinspace FF\thinspace FF\thinspace FF$ 上表中的 $0x3F\thinspace 3F\thinspace 3F\thinspace 3F$ 是一个很有用的数值,它有两个特性: 它的两倍不超过 $0x7F\thinspace FF\thinspace FF\thinspace FF$,即 int 能表示的最大正整数。 整数的每 8 位(每个字节)都是相同的。 我们常用的 memset(a, val, sizeof(a)) 初始化一个 int 数组 $a$ 时,把 $val(0x00\sim 0xFF)$ 填充到数组 $a$ 的每个字节上,而一个 int 占用 4 个字节,所以用 memset 只能赋值出每 8 位都相同的 int。 所以,当我们想把一个数组中的数值初始化为正无穷时,为了避免加法上溢或者繁琐的判断,可以用 memset(a, 0x3f, sizeof(a)) 给数组赋 $0x3F\thinspace 3F\thinspace 3F\thinspace 3F$ 的值。 左移 在二进制表示下把数字同时向左移动,低位以 $0$ 填充,高位越界后舍弃。 在二进制补码表示下把数字同时向右移动,高位以符号位填充,低位越界后舍弃。 $$ n»1 = \lfloor \frac{n}{2.0} \rfloor $$ 注意,整数除法在 C++ 中的实现是向零取整(舍弃小数部分),例如 $(-3)/2=-1$,$3/2=1$。 在二进制补码表示下把数字同时向右移动,高位以 $0$ 填充,低位越界后舍弃。 无符号整数右移使用的是逻辑右移。对于有符号整数,在 C++ 20 前并没有规定使用算术右移还是逻辑右移(大多数平台上进行算术右移)。C++ 20 开始才规定使用算术右移。 《算法竞赛进阶指南》 补码的计算方法 - Murphy - 知乎 cppreference.com

2022/8/21
articleCard.readMore

李超线段树笔记

线段树之标记永久化 普通的线段树在做区间修改时依赖懒标记(lazy tag),当我们从一个点向下访问时,需要将标记 pushdown。能否避免如此多的 pushdown 操作呢?这时需要用到标记永久化技巧。 我们要做的就是将 lazy tag 永久地留在当前的结点,这时子树中的所有结点都不会被这个 tag 所影响。因此,子树中询问的最大值 = 实际最大值 - tag。当我们想得到正确答案时,只要将子树返回的最大值加上当前 tag 即可。 标记永久化存在局限性,需要满足不同的修改操作可以交换顺序,或者说对答案的贡献是独立的这一条件。 举个例子:区间设置+区间加法,先设置后加和先加后设置的结果是不一样的,因此不能交换顺序。如果使用标记永久化,就可能改变了这个顺序。 比如我们先设置后加,并且令设置的区间比加的区间大,因此加的 tag 在下方,设置的 tag 在上方。 根据前面的方法,我们会从下往上取 tag,也就是先加法,再设置。 这时我们发现,由于上层是一个设置操作,下面的所有答案最终都变成了设置的那个数字,下层操作就失效了。显然有问题。 总结:标记永久化就是不再下放标记,而是让标记永久地停留在当前结点上。在统计答案时再考虑标记的影响。 复杂度分析:由于标记不会下放,但如果有两个标记落在了一个结点上,我们不会分别存储这两个标记,而是加起来合成一个标记($(+2) + (+3) = (+5)$)。因此,每个结点最多只有一个标记。询问时最多考虑 $\log n$ 个标记,复杂度和普通线段树相同 $O(\log n)$。 程序实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int up(int p) { tree[p].mx = max(tree[p<<1].mx, tree[p<<1|1].mx)+tree[p].tag; } int query(int p, int l, int r, int x, int y) { if (l >= x && r <= y) return tree[p].mx; int mid = (l + r) / 2, ans = -INF; if (x <= mid) ans = max(ans, query(p << 1, l, mid, x, y)); if (y > mid) ans = max(ans, query(p << 1 | 1, mid + 1, r, x, y)); return ans+tree[p].tag; } void update(int cur, int l, int r, int x, int y, int c) { if (l >= x && r <= y) { tree[cur].tag += c; tree[cur].mx += c; return; } int mid = (l + r) / 2; if (x <= mid) update(cur << 1, l, mid, x, y, c); if (y > mid) update(cur << 1 | 1, mid + 1, r, x, y, c); up(cur); } 例题:Luogu-4097 [HEOI2013]Segment 要求在平面直角坐标系下维护两个操作(强制在线): 在平面上加入一条线段。记第 $i$ 条被插入的线段的标号为 $i$,该线段的两个端点分别为 $(x_0,y_0)$,$(x_1,y_1)$。 给定一个数 $k$,询问与直线 $x = k$ 相交的线段中,交点纵坐标最大的线段的编号(若有多条线段与查询直线的交点纵坐标都是最大的,则输出编号最小的线段)。特别地,若不存在线段与给定直线相交,输出 $0$。 操作总数 $1 \leq n \leq 10^5$,$1 \leq k, x_0, x_1 \leq 39989$,$1 \leq y_0, y_1 \leq 10^9$。 将问题转化成以下的操作: 加入一个一次函数,定义域为 $[l,r]$; 给定 $k$,求定义域包含 $k$ 的所有一次函数中,在 $x=k$ 处取值最大的那个,如果有多个函数取值相同,选编号最小的。 注意:当线段垂直于 $y$ 轴时,如果按照一般的式子计算,会出现除以零的情况。假设线段两端点分别为 $(x,y_0)$ 和 $(x,y_1)$,$y_0<y_1$,则插入定义域为 $[x,x]$ 的一次函数 $f(x)=0\cdot x+y_1$。 首先,我们建立一棵线段树,每个结点代表一段 $x$ 轴上的区间,结点的懒标记表示一条线段,记为 $g$。 现在我们要插入线段 $f$。一直找到被 $f$ 完全覆盖的区间,进行分类讨论。 如图,按新线段 $f$ 取值是否大于原标记 $g$,我们可以把当前区间分为两个子区间。其中 肯定有一个子区间被左区间或右区间完全包含,也就是说,在两条线段中,肯定有一条线段,只可能成为左区间的答案,或者只可能成为右区间的答案。我们用这条线段递归更新对应子树,用另一条线段作为懒标记更新整个区间,这就保证了递归下传的复杂度。当一条线段只可能成为左或右区间的答案时,才会被下传,所以不用担心漏掉某些线段。 具体来说,设当前区间的中点为 $mid$,我们拿新线段 $f$ 与原最优线段 $g$ 与 $x=mid$ 的交点的值作比较。 如果新线段 $f$ 更优,则将 $f$ 和 $g$ 交换。转化成了第 2 种情况。 对于中点处 $f$ 不如 $g$ 优的情况: 若在左端点处 $f$ 更优,那么 $f$ 和 $g$ 必然在左半区间中产生了交点,$f$ 只有在左区间才可能优于 $g$,递归到左儿子中进行下传; 若在右端点处 $f$ 更优,那么 $f$ 和 $g$ 必然在右半区间中产生了交点,$f$ 只有在右区间才可能优于 $g$,递归到右儿子中进行下传; 若在左、右端点处 $g$ 都更优,那么 $f$ 不可能成为答案,不需要继续下传。 除了这两种情况之外,还有一种情况是 $f$ 和 $g$ 刚好交于中点,在程序实现时可以归入 $f$ 不如 $g$ 优的情况,结果会往 $f$ 更优的一个端点进行递归下传。 最后将 $g$ 作为当前区间的懒标记。 查询时,用到标记永久化。我们只需要找到所有覆盖了 $k$ 的区间,每次考虑这个标记对答案有怎样的贡献即可。 复杂度分析:查询的时间复杂度显然为 $O(\log n)$,而插入过程中,我们需要将原线段拆分到 $O(\log n)$ 个区间中,对于每个区间,我们又需要花费 $O(\log n)$ 的时间递归下传,从而插入过程的时间复杂度为 $O(\log^2 n)$。 RECORD 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 #include <bits/stdc++.h> using namespace std; const int N = 1000005; const int MOD1 = 39989; const int MOD2 = 1000000000; const double EPS = 1e-12; struct line { double k, b; int l, r; int id; } tree[N << 2]; int n; double calc(line a, int x) { // 计算纵坐标 return x * a.k + a.b; } // 注意 cmp 写法。不要写成 a-b < EPS。 int cmp(double a, double b) { if (a - b > EPS) return 1; if (b - a > EPS) return -1; return 0; } void build(int rt, int l, int r) { tree[rt] = {0, 0, 1, 40000, 0}; if (l == r) return; int m = (l + r) >> 1; build(rt << 1, l, m); build(rt << 1 | 1, m + 1, r); } void modify(int root, int l, int r, line k) { if (l >= k.l && r <= k.r) { // 对线段完全覆盖到的区间进行修改 // g: tree[root]; f: k. int m = (l + r) >> 1; if (cmp(calc(tree[root], m), calc(k, m)) == -1) swap(tree[root], k); int cl = cmp(calc(k, l), calc(tree[root], l)), cr = cmp(calc(k, r), calc(tree[root], r)); // 3. 若在左右端点处 g 都更优,那么 f 不可能成为答案,不需要继续下传。 if (cl == -1 && cr == -1) return; // 1. & 2. 注意题目要求下标尽量小,下标小的线段即使不更优,如果值相等,也可以下传。 if (cl == 1 || (cl == 0 && k.id < tree[root].id)) modify(root << 1, l, m, k); if (cr == 1 || (cr == 0 && k.id < tree[root].id)) modify(root << 1 | 1, m + 1, r, k); } else { int m = (l + r) >> 1; if (k.l <= m) modify(root << 1, l, m, k); if (k.r > m) modify(root << 1 | 1, m + 1, r, k); } } pair<double, int> pmax(pair<double, int> x, pair<double, int> y) { // 注意题目要求下标尽量小 if (cmp(x.first, y.first) == -1) return y; else if (cmp(x.first, y.first) == 1) return x; else return x.second < y.second ? x : y; } pair<double, int> query(int root, int l, int r, int x) { if (l == r) return {calc(tree[root], x), tree[root].id}; int m = (l + r) >> 1; pair<double, int> ans = {calc(tree[root], x), tree[root].id}; // 注意标记永久化,取 max 比较 if (x <= m) ans = pmax(ans, query(root << 1, l, m, x)); else ans = pmax(ans, query(root << 1 | 1, m + 1, r, x)); return ans; } int main() { ios::sync_with_stdio(false); cin >> n; build(1, 1, 40000); int lastans = 0; int segi = 0; while (n--) { int op; cin >> op; if (op == 0) { int k; cin >> k; k = (k + lastans - 1 + MOD1) % MOD1 + 1; lastans = query(1, 1, 40000, k).second; cout << lastans << endl; } else { int a, b, x, y; cin >> a >> b >> x >> y; a = (a + lastans - 1 + MOD1) % MOD1 + 1; b = (b + lastans - 1 + MOD2) % MOD2 + 1; x = (x + lastans - 1 + MOD1) % MOD1 + 1; y = (y + lastans - 1 + MOD2) % MOD2 + 1; if (a > x) { swap(a, x); swap(b, y); } line t; if (a == x) { // 垂直于 y 的函数的特殊处理 t.k = 0; t.b = max(b, y); } else { t.k = (double)(y - b) / (x - a); t.b = b - t.k * a; } t.l = a; t.r = x; t.id = ++segi; modify(1, 1, 40000, t); } } return 0; } Luogu-P4254 [JSOI2008]Blue Mary 开公司 本题给出的是直线,比例题更简单 李超线段树 - OI Wiki CC BY-SA 4.0

2022/8/16
articleCard.readMore

树状数组笔记

早就学习过线段树了,但惭愧的是更简单的树状数组却一直没有深入理解过,仅仅停留在背代码的层级。今天认真学习一下树状数组。 树状数组(Binary Index Tree, BIT / Fenwick Tree)支持单点修改和区间查询两种简单操作,时间复杂度均为 $O(\log n)$。它的实现比线段树简单,速度更快,但功能稍逊一筹。 我们用 $C_i$ 来表示 $A$ 数组的一段区间,定义 $x$ 的二进制表示中,最低位的 $1$ 的位置为 $\operatorname{lowbit}(x)$,那么用 $C_i$ 代表 $A$ 数组的下标区间 $[i-\operatorname{lowbit}(i)+1, i]$。举个例子,$4_{(10)} = 100_{(2)}$,$100_{(2)}-\operatorname{lowbit}(100_{(2)})+1_{(2)}=1_{(2)}=1_{(10)}$,那么 $C_4$ 代表的区间就是 $[1, 4]$。通过这样的设计,树状数组将结点数压缩到与数组长度相同,不像线段树一样需要 $2n$ 个结点。 之所以会有这个特点,是因为对于位置 $i$,其对应的结点所在的高度就是 $\operatorname{lowbit}(i)$ 的位数。第一层结点为全体 $2^0 + 2^1k$,即所有 $\operatorname{lowbit}(i)=1$ 的数字;第二层结点为全体 $2^1 + 2^2k$ ,即所有 $\operatorname{lowbit}(i)=2$ 的数字;第三层结点为全体 $2^2 + 2^3k$ ,即所有 $\operatorname{lowbit}(i)=4$ 的数字;以此类推。也就是说,对于位置 $i$,在这个位置往上垂直追溯,能追溯的层数就是 $i$ 的二进制表示的末尾 $0$ 数量。而结点高度又决定了其子树的大小,于是它所代表的信息区间大小也就一定是 $2^{i的末尾0数量}=\operatorname{lowbit}(i)$。 *来源于参考资料 1 $\operatorname{lowbit}$ 如何计算呢?我们有这样一条公式:$\operatorname{lowbit}(x)=(x)&(-x)$。在计算机中,有符号数采用补码表示。在补码表示下,$x$ 的相反数 -x = ~x + 1,也就是按位非再加一。例如 $x$ 的最后一个 $1$ 的位置附近是 $\cdots 01000\cdots$,按位非之后是 $\cdots 10111\cdots$,加一再变成 $\cdots 11000\cdots$;而前面每一位都与原来相反。这时我们再把它和 $x$ 按位与,得到的结果为 $01000\cdots$ 即 $\operatorname{lowbit}(x)$。 修改操作类似于向上爬树的过程。例如我们要修改第三个元素,那么依次要修改 $C_3, C_4, C_8$,用二进制表示就是 $11, 100, 1000$,发现下标 $x$ 每次都要 $x += \operatorname{lowbit}(x)$,直到到达顶部。 1 2 3 4 5 6 int add(int x, int y) { while (x <= n) { tree[x] += y; x += lowbit(x); } } 例如查询第六个元素的前缀和,即 $[1, 6]$ 这一区间。首先 $C_6$ 表示的区间是 $[5, 6]$,然后跳到 $C_4$,代表的区间是 $[1, 4]$,加起来就 OK 了。从 $C_6$ 到 $C_4$ 是怎么跳的呢?实际上是 $110 - \operatorname{lowbit}(110) = 100$。所以下标 $x$ 每次都要 $x -= \operatorname{lowbit}(x)$,直到 $x<1$。 1 2 3 4 5 6 7 8 int query(int x) { int sum = 0; while (x) { sum += tree[x]; x -= lowbit(x); } return sum; } 至于求 $[a, b]$ 这一区间的和,只需要分别求 $[1, b]$ 和 $[1, a)$ 再相减即可。 暴力的建树方法是进行 $n$ 次单点修改,时间复杂度为 $O(n\log n)$。 有两种方法可以实现 $O(n)$ 建树。 前面讲到 $C_i$ 表示的区间是 $[i-\operatorname{lowbit}(i)+1, i]$,那么我们可以先预处理一个 $sum$ 前缀和数组,再计算 $C$ 数组。 1 2 3 4 5 void init() { for (int i = 1; i <= n; ++i) { C[i] = sum[i]-sum[i-lowbit(i)]; } } 我们很容易知道当前点 $i$ 的父亲是 $i+\operatorname{lowbit}(i)$,所以用自己的值更新父亲即可。 1 2 3 4 5 6 7 void init() { for (int i = 1; i <= n; ++i) { C[i] += a[i]; int j = i + lowbit(i); if (j <= n) C[j] += C[i]; } } 树状数组的原理是什么? - SleepyBag 的回答 - 知乎 算法学习笔记(2) : 树状数组 - Pecco 的文章 - 知乎

2022/8/15
articleCard.readMore

斜率优化 DP 笔记

X(j) 和斜率均单调的斜率优化 这是第一次学斜率优化学会的。 Luogu LOJ 题目描述: P 教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中。 P 教授有编号为 $1 \cdots n$ 的 $n$ 件玩具,第 $i$ 件玩具经过压缩后的一维长度为 $C_i$。 为了方便整理,P 教授要求: 在一个一维容器中的玩具编号是连续的。 同时如果一个一维容器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物。形式地说,如果将第 $i$ 件玩具到第 $j$ 个玩具放到一个容器中,那么容器的长度将为 $x=j-i+\sum\limits_{k=i}^{j}C_k$。 制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为 $x$,其制作费用为 $(x-L)^2$。其中 $L$ 是一个常量。P 教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过 $L$。但他希望所有容器的总费用最小。 $1 \leq n \leq 5 \times 10^4$,$1 \leq L \leq 10^7$,$1 \leq C_i \leq 10^7$。 令状态 $f(i)$ 表示把前 $i$ 个玩具装箱的最小费用,$s(i)$ 为 $c_i$ 的前缀和。 假如将玩具 $j$ 到 $i$ 装在同一箱子,容易列出状态转移方程 $f(i) = \min_{1\le j\le i}{f(j-1)+(i-j+s(i)-s(j-1)-L)^2}$。 直接算的话时间复杂度是 $O(n^2)$,超时。 在计算过程中,$i, s(i), L$ 这些项是已知的,而含有 $j$ 的项如 $f(j-1), j, s(j-1)$ 都是未知的。展开平方式,进行参数分离,定义 $g(i) = i+s(i)-L$,$x(j) = j+s(j-1)$。可选决策 $j$ 的费用记为 $f(i)_j$,则有 $$ f(i)_j = f(j-1)+(g(i)-x(j))^2 = f(j-1) + g(i)^2 - 2 \times g(i) \times x(j) + x(j)^2 $$ 式子中 $f(j-1), g(i)^2, x(j)^2$ 这三项中 $i$ 与 $j$ 这两个参数完全分离,但 $2 \times g(i) \times x(j)$ 却没有分离,不像普通单调队列优化的题目那样 「$j_2 > j_1, f(i){j{2}} < f(i){j{1}}$,$j_1$ 就可以删除」这么明显的单调性,需要深入分析。 研究 $j_2 > j_1$ 时 $f(i){j{2}} < f(i){j{1}}$ 即「决策 $j_2$ 优于 决策 $j_1$」的前提条件: $$ \begin{aligned} f(j_2-1) + g(i)^2 - 2 \times g(i) \times x(j_2) + x(j_2)^2 & < f(j_1-1) + g(i)^2 - 2 \times g(i) \times x(j_1) + x(j_1)^2 \\ (f(j_2-1) + x(j_2)^2) - (f(j_1-1) + x(j_1)^2) & < 2 \times g(i) \times (x(j_2) - x(j_1)) \end{aligned} $$ 再令 $y(i) = f(i-1)+x(i)^2$,因 $x(i) = i+s(i-1)$ 是单调递增的,所以 $x(j_2)-x(j_1) > 0$,所以有 $$ \frac{y(j_2) - y(j_1)}{x(j_2)-x(j_1)} < 2 \times g(i) $$ 上面式子像 $j_1, j_2$ 两个点形成的斜率,于是用 斜率优化。 斜率优化:计算 $f(i)$ 时,可选决策 $j_2 > j_1$,如果 $j_2$ 比 $j_1$ 优,令 $T(j_1, j_2) = \frac{y(j_2)-y(j_1)}{x(j_2)-x(j_1)}$,则必须满足 $T(j_1, j_2) < 2 \times g(i)$。 该题 $x(i), g(i)$ 都是单调上升的。下面有两个重要结论: 计算 $f(i)$ 时,所有可选决策是 $1, \cdots, i$,可以删除其中的冗余决策,使得队列中从小到大依次存储有价值的可选决策 $j_1, j_2, \cdots, j_k$,使得这些相邻决策之间的斜率都要大于 $2 \times g(i)$。即: $T(j_1, j_2) > 2 \times g(i), T(j_2, j_3) > 2 \times g(i), \cdots, T(j_{k-1}, j_k) > 2 \times g(i)$。最优决策是队首元素 $j_1$。 证明:如果队列中相邻两个决策 $x, y$ 之间的斜率 $T(x, y) < 2 \times g(i)$,由于 $g(i)$ 是单调增的,则对于后面的 $i_1(i_1 > i)$,计算 $f(i_1)$ 时,有:$T(x, y) < 2 \times g(i) < 2 \times g(i_1)$,所以 $y$ 永远比 $x$ 优,$x$ 可以删除。所以 $T(j_1, j_2) > 2 \times g(i), T(j_2, j_3) > 2 \times g(i), \cdots, T(j_{k-1}, j_k) > 2 \times g(i)$。则对于 $f(i)$ 来说,$j_1$ 比 $j_2$ 优,$j_2$ 比 $j_3$ 优,…,$j_{k-1}$ 比 $j_k$ 优,所以队首 $j_1$ 是最优的。并且在 $f(i+1)$ 时可以在之前维护的队列基础上加入新的决策 $i+1$,再继续维护。 可以维护相邻决策之间的斜率单调增。 证明:设队列中三个相邻决策 $j_1, j_2, j_3$,假设出现相邻斜率单调递减的情况,即 $T(j_1, j_2) > T(j_2, j_3)$,分析 $j_2$ 有没有可能在计算 $f$ 的过程中成为最优决策。 $$ j_2 比 j_1 优 \Rightarrow T(j_1, j_2) < 2 \times g(i) \\ j_2 比 j_3 优 \Rightarrow T(j_2, j_3) > 2 \times g(i) \\ T(j_1, j_2) < 2 \times g(i) < T(j_2, j_3) $$ 矛盾。所以 $j_2$ 不可能成为最优决策,可以删除。得证。 综上: 应该维护队列中相邻决策满足:$2 \times g(i) < T(j_1, j_2) < T(j_2, j_3) < \cdots < T(j_{k-1}, j_k)$。 最优决策取队首元素。 程序实现: 删尾:要插入新的可选决策 $i$,每次选队列最后两个决策 $j_{k-1}, j_k$,如果 $T(j_{k-1}, j_k) > T(j_k, i)$,则删除队尾元素 $j_k$。循环做下去,直到队列中的元素个数小于 $2$ 或者 $T(j_{k-1}, j_k)<T(j_k, i)$; 插入:把新的可选决策 $i$ 加入队尾; 删头:取队首两个决策 $j_1, j_2$,如果 $T(j_1, j_2) < 2 \times g(i)$,则删除队首元素 $j_1$。如此循环下去,直到队列中元素个数小于 $2$ 或者 $T(j_1, j_2) > 2 \times g(i)$; 取头:最优决策在队首。 时间复杂度是 $O(n)$。 RECORD 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 #include <bits/stdc++.h> using namespace std; #define LL long long int n, L; LL s[50050], dp[50050]; int head, tail, Q[50050]; LL x(int i) { return i+s[i-1]; } LL y(int i) { return dp[i-1]+x(i)*x(i); } LL g(int i) { return i+s[i]-L; } double T(int i, int j) { return (double)(y(j)-y(i))/(x(j)-x(i)); } LL cost(int i, int j) { return abs((j-i+s[j]-s[i-1]-L)*(j-i+s[j]-s[i-1]-L)); } int main() { cin >> n >> L; for (int i = 1; i <= n; i++) { cin >> s[i]; s[i] += s[i-1]; } head = 1; tail = 0; for (int i = 1; i <= n; i++) { while (head <= tail-1 && T(Q[tail-1], Q[tail]) > T(Q[tail], i)) tail--; Q[++tail] = i; while (head <= tail-1 && T(Q[head], Q[head+1]) < 2*g(i)) head++; dp[i] = dp[Q[head]-1]+cost(Q[head], i); } cout << dp[n] << endl; return 0; } Luogu LOJ 题目描述: 你有一支由 $n$ 名预备役士兵组成的部队,士兵从 $1$ 到 $n$ 编号,你要将他们拆分成若干特别行动队调入战场。出于默契的考虑,同一支特别行动队中队员的编号应该连续,即为形如 $(i, i + 1, \cdots i + k)$的序列。所有的队员都应该属于且仅属于一支特别行动队。 编号为 $i$ 的士兵的初始战斗力为 $x_i$ ,一支特别行动队的初始战斗力 $X$ 为队内士兵初始战斗力之和,即 $X = x_i + x_{i+1} + \cdots + x_{i+k}$。 通过长期的观察,你总结出对于一支初始战斗力为 $X$ 的特别行动队,其修正战斗力 $X’= aX^2+bX+c$,其中 $a,~b,~c$ 是已知的系数($a < 0$)。 作为部队统帅,现在你要为这支部队进行编队,使得所有特别行动队的修正战斗力之和最大。试求出这个最大和。 $1 \leq n \leq 10^6$,$-5 \leq a \leq -1$,$-10^7 \leq b \leq 10^7$,$-10^7 \leq c \leq 10^7$,$1 \leq x_i \leq 100$。 令 $f(i)$ 表示把前 $i$ 个士兵编队的最大修正战斗力之和,$s(i)$ 表示 $x_i$ 的前缀和。 假如将士兵 $j$ 到 $i$ 编在同一队,得出状态转移方程 $f(i) = \max_{1\le j\le i}{f(j-1) + a\times (s(i)-s(j-1))^2 + b\times (s(i)-s(j-1)) + c}$。 展开后得出 $$ f(i) = \max_{1\le j\le i}{f(j-1) + a \times s(i)^2 + a \times s(j-1)^2 - 2a \times s(i) \times s(j-1) + b \times s(i) - b \times s(j-1) + c } \\ f(i) = \max_{1\le j\le i}{f(j-1) + a \times (s(i)^2 + s(j-1)^2) - 2a \times s(i) \times s(j-1) + b \times (s(i) - s(j-1)) + c } $$ 研究 $j_2 > j_1$ 时 $f(i){j{2}} > f(i){j{1}}$ 即「决策 $j_2$ 优于 决策 $j_1$」的前提条件: $$ \begin{aligned} f(j_2-1) + a \times (s(i)^2 + s(j_2-1)^2) - 2a \times s(i) \times s(j_2-1) + b \times (s(i) - s(j_2-1)) + c & > f(j_1-1) + a \times (s(i)^2 + s(j_1-1)^2) - 2a \times s(i) \times s(j_1-1) + b \times (s(i) - s(j_1-1)) + c \\ f(j_2-1) - f(j_1-1) + a \times (s(i_2-1)^2 - s(j_1-1)^2) - b \times (s(j_2-1) - s(j_1-1)) & > 2a \times s(i) \times (s(j_2-1) - s(j_1-1)) \\ \frac{f(j_2-1) - f(j_1-1) + a \times (s(i_2-1)^2 - s(j_1-1)^2) - b \times (s(j_2-1) - s(j_1-1))}{s(j_2-1) - s(j_1-1)} & > 2a \times s(i) \end{aligned} $$ 令 $g(i) = f(i-1) + a \times s(i-1)^2 - b \times s(i-1)$,则有 $$ \frac{g(j_2)-g(j_1)}{s(j_2-1) - s(j_1-1)} > 2a \times s(i) $$ 上面式子就是 $(s(j_1-1), g(j_1)), (s(j_2-1), g(j_2))$ 两个点形成的斜率。 小结:计算 $f(i)$ 时,可选决策 $j_2 > j_1$,令 $T(j_1, j_2)=\frac{g(j_2)-g(j_1)}{s(j_2-1) - s(j_1-1)}$,如果 $j_2$ 比 $j_1$ 优,必须满足 $T(j_1, j_2) > 2a \times s(i)$。 该题 $s(i)$ 单调增,可以得出两个结论: 计算 $f(i)$ 时,可以删除冗余决策,使得队列中从小到大依次存储有价值的可选决策 $j_1, j_2, \cdots, j_k$,使得这些相邻决策之间的斜率都要小于 $2a \times s(i)$。即: $T(j_1, j_2) < 2a \times s(i), T(j_2, j_3) < 2a \times s(i), \cdots, T(j_{k-1}, j_k) < 2a \times s(i)$。最优决策是队首元素 $j_1$。 证明:对于队列中两个相邻决策 $x, y$,如果 $T(x, y) > 2a \times s(i)$,由于 $s(i)$ 单调增,且题目中规定 $a < 0$,所以 $2a \times s(i)$ 单调下降,所以对于 $i_1 > i$ 有 $T(x, y) > 2a \times s(i) > 2a \times s(i_1)$,$y$ 永远比 $x$ 优,所以 $x$ 可以删除。所以 $T(j_1, j_2) < 2a \times s(i), T(j_2, j_3) < 2a \times s(i), \cdots, T(j_{k-1}, j_k) < 2a \times s(i)$,$j_1$ 比 $j_2$ 优,$j_2$ 比 $j_3$ 优,…,$j_k-1$ 比 $j_k$ 优。所以队首元素 $j_1$ 是最优的。 可以维护相邻决策之间的斜率单调减。 证明:设队列中三个相邻决策 $j_1, j_2, j_3$,假设出现单调增即 $T(j_1, j_2) < T(j_2, j_3)$,分析 $j_2$ 有没有可能成为最优决策。 $$ j_2 比 j_1 优 \Rightarrow T(j_1, j_2) > 2a \times s(i) \\ j_2 比 j_3 优 \Rightarrow T(j_2, j_3) < 2a \times s(i) \\ T(j_2, j_3) < 2a \times s(i) < T(j_1, j_2) $$ 矛盾。$j_2$ 不可能成为最优决策,可以删除。得证。 综上: 应该维护队列中相邻决策满足:$T(j_1, j_2) > T(j_2, j_3) > \cdots > T(j_{k-1}, j_k) > 2a \times s(i)$。 最优决策取队首元素。 RECORD 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 #include <bits/stdc++.h> using namespace std; #define LL long long int n; LL a, b, c; int st, en, Q[1000006]; LL s[1000006], dp[1000006]; LL g(int i) { return dp[i - 1] + a * s[i - 1] * s[i - 1] - b * s[i - 1]; } double T(int i, int j) { return (g(j) - g(i)) * 1.0 / (s[j - 1] - s[i - 1]) * 1.0; } LL cost(int i, int j) { return a * (s[i] - s[j - 1]) * (s[i] - s[j - 1]) + b * (s[i] - s[j - 1]) + c; } int main() { cin >> n >> a >> b >> c; for (int i = 1; i <= n; i++) { cin >> s[i]; s[i] += s[i - 1]; } st = 1; en = 0; for (int i = 1; i <= n; i++) { while (st <= en - 1 && T(Q[en - 1], Q[en]) <= T(Q[en], i)) en--; Q[++en] = i; while (st <= en - 1 && T(Q[st], Q[st + 1]) >= 2 * a * s[i]) st++; dp[i] = dp[Q[st] - 1] + cost(i, Q[st]); } cout << dp[n] << endl; return 0; } 第二次学习斜率优化。 决策点横坐标不单调,维护凸包就不能用单调队列了,因为插入点时可能会插到凸包点集中间的某个位置,而队列不支持这种操作,需要用到平衡树维护或者用 CDQ 分治保证单调性。这里有计算几何基础的话会更易理解,因为上面维护图形时的删点操作与水平序 Graham 扫描法求凸包是类似的,而扫描法的前提为:点集呈水平序,即点从左至右依次排列(体现为 $X(j)$ 单调不减)。(source) 如果斜率 $k0[i]$ 不存在单调性该怎么办?(假设此时 $X(j)$ 仍然单调) 我们仍可以用队列维护凸包点集,但不知道每一次会在什么地方取得最优决策点,所以必须要保留整个凸包以确保决策有完整的选择空间,也就是说不能弹走队首,同时查找答案也不能直接取队首,只能使用二分。 Luogu-P5785 [SDOI2012]任务安排 列出状态转移方程:$f_i = f_j + s*(cs_n-cs_j)+ts_i*(cs_i-cs_j)$ 变换一下得到:$f_j=(s+ts_i)cs_j+f_i-cs_its_i-s*cs_n$ 令 $y=f_j, k=(s+ts_i), x=cs_j, b=f_i-cs_its_i-scs_n$,就可以得到一次函数解析式。 转化的方法是把外循环时不能直接得出的(与 $j$ 有关)放在 $x,y$,可以依靠外指针 $i$ 得到的放在 $k,b$。 我们把 $j$ 称作一个决策点,每次选择一个最优决策点来更新 $f_i$ 的值。 对于一个决策点所对应的直线,都可以求得截距 $b$,又因为 $b=f_i-cs_its_i-scs_n$ 中 $cs_its_i-scs_n$ 是固定的,因此 $b$ 越小,$f_i$ 越小。为了让 $f_i$ 尽量小,我们就要求出最小的 $b$。 然后分析单调性。待决策点的横坐标 $x=cs_j$ 是单调递增的。但是由于 $t_i$ 可能为负,斜率 $k=(s+ts_i)$ 不单调。我们依然可以用单调队列(事实上是一个单调栈)维护凸包点集,上凸点依然一定不会成为最优决策点,需维护下凸包。但与前面不一样的地方是不知道当前最优决策点在哪里,所以必须保留整个凸包,不能弹出队首。查找答案时不能直接取队首,要二分查找这样的点:它与左右决策点连线的斜率 $k_1 \lt k \lt k_2$。 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 LL s, t[N], f[N], c[N], cs[N], ts[N]; LL K(int i) { return s+ts[i]; } LL X(int i) { return cs[i]; } LL Y(int i) { return f[i]; } int search(int L, int R, LL S) { int ret = 0; while (L<=R) { int M = (L+R)>>1; if (Y(Q[M+1])-Y(Q[M]) > S*(X(Q[M+1])-X(Q[M]))) ret = M, R = M-1; else L = M+1; } return ret; } int main() { cin >> n >> s; for (int i = 1; i <= n; i++) { cin >> t[i] >> c[i]; ts[i] = ts[i-1]+t[i], cs[i] = cs[i-1]+c[i]; } st = 1, ed = 0; Q[++ed] = 0; for (int i = 1; i <= n; i++) { int j = Q[search(st, ed, K(i))]; f[i] = Y(j)+s*(cs[n]-cs[j])+ts[i]*(cs[i]-cs[j]); while (st < ed && (Y(Q[ed])-Y(Q[ed-1]))*(X(i)-X(Q[ed])) // 不求斜率而是用乘积的形式比较大小,避免除法产生的精度问题 >= (Y(i)-Y(Q[ed]))*(X(Q[ed])-X(Q[ed-1]))) ed--; Q[++ed] = i; } cout << f[n] << endl; return 0; } Luogu-P4655 [CEOI2017] Building Bridges 列出状态转移方程 $f_i=f_j+h_i^2+h_j^2-2h_ih_j+w_{i-1}-w_j$。 这道题如果用斜率优化做,就要写 CDQ 了,感觉很麻烦。于是换一种思路,把方程变成这样的形式:$f_i = h_i^2+w_{i-1}-2h_jh_i+h_j^2+f_j-w_j$。令 $k=-2h_j, x=h_i, b=h_j^2+f_j-w_j$,就得到 $f_i = h_i^2+w_{i-1}+kx+b$。为了让 $f_i$ 最小,而方程前半部分又是固定的,问题就转化成求 $x=h_i$ 时 $y=kx+b$ 的最小值。可以用李超线段树优化,时间复杂度为 $O(n \log n)$。 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 const int N = 100005, M = 1000006; int n, t[4*M]; LL h[N], w[N], f[N]; struct line { LL k, b; } a[N]; LL calc(int id, LL x) { return a[id].k*x+a[id].b; } void upd(int u, int l, int r, int p) { if (l == r) { if (calc(p, l) < calc(t[u], l)) t[u] = p; return; } int mid = (l+r)>>1; if (calc(p, mid) < calc(t[u], mid)) swap(t[u], p); if (calc(p, l) < calc(t[u], l)) upd(u<<1, l, mid, p); else if (calc(p, r) < calc(t[u], r)) upd(u<<1|1, mid+1, r, p); } LL qry(int u, int l, int r, LL p) { if (l == r) return calc(t[u], p); int mid = (l+r)>>1; LL ans = calc(t[u], p); if (p <= mid) ans = min(ans, qry(u<<1, l, mid, p)); else ans = min(ans, qry(u<<1|1, mid+1, r, p)); return ans; } int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> h[i]; a[0].b = 1e18; // 因为线段树数组所有元素的初值为 0,要给 a[0].b 赋极大值避免影响 for (int i = 1; i <= n; i++) { cin >> w[i]; w[i] = w[i-1]+w[i]; } a[1].k = -2*h[1]; a[1].b = h[1]*h[1]+f[1]-w[1]; upd(1, 0, M, 1); for (int i = 2; i <= n; i++) { f[i] = h[i]*h[i]+w[i-1]+qry(1, 0, M, h[i]); a[i].k = -2*h[i]; a[i].b = h[i]*h[i]+f[i]-w[i]; upd(1, 0, M, i); } cout << f[n] << endl; return 0; } CDQ 的过程:首先将当前区间按照 $id$ 的大小分成左右两个子区间,两个区间内分别按照斜率排序。然后先递归处理左边,结束后左边的 $X(j)$ 是递增的,此时右边还没处理,所以右边的斜率是递增的,然后就转化成最基础的斜率优化了。把左边的点拿出来维护凸包(用单调队列),依次更新右边的点的答案,再递归处理右边,最后将整个区间按照 $X(j)$ 从小到大排序即可。 在计算斜率的函数 double T() 中,一定要用 (double) 或者 *1.0,将 long long 变成 double 类型之后再计算!!!!! 用 >=,<= Q[st] 老是写成 st。 队列初始化大多都要塞入一个点 P(0),比如 玩具装箱 toy,需要塞入 P(S[0],dp[0]+(S[0]+L)2) 即 P(0,0),其代表的决策点为 j=0。手写队列的初始化是 st=1,ed=0,由于塞了初始点导致 ed 加 1,所以在一些题解中可以看到 h=t=1 甚至是 h=t=0,h=t=2 之类的写法,其实是因为省去了塞初始点的代码。它们都是等价的。 当 $X(j)$ 非严格递增时,在求斜率时可能会出现 $X(j1)=X(j2)$ 的情况,此时最好是写成这样的形式:return Y(j)>=Y(i)?inf:-inf,而不要直接返回 inf 或者 −inf,在某些题中情况较复杂,如果不小心画错了图,返回了一个错误的极值就完了,而且这种错误只用简单数据还很难查出来。 【学习笔记】动态规划—斜率优化DP(超详细) - 辰星凌的博客 强烈推荐!!! DP的各种优化(动态规划,决策单调性,斜率优化,带权二分,单调栈,单调队列) - FlashHu 🙇‍

2022/8/14
articleCard.readMore

Luogu-P3521 「POI2011」ROT-Tree Rotations

题意 给定一颗有 $n$ 个叶节点的二叉树。每个叶节点都有一个权值 $p_i$(注意,根不是叶节点),所有叶节点的权值构成了一个 $1 \sim n$ 的排列。 对于这棵二叉树的任何一个结点,保证其要么是叶节点,要么左右两个孩子都存在。 现在你可以任选一些节点,交换这些节点的左右子树。 在最终的树上,按照先序遍历遍历整棵树并依次写下遇到的叶结点的权值构成一个长度为 $n$ 的排列,你需要最小化这个排列的逆序对数。 $2 \leq n \leq 2 \times 10^5$, $0 \leq x \leq n$,所有叶节点的权值是一个 $1 \sim n$ 的排列。 按照先序遍历整棵树,取叶结点,用人话说就是从左到右取叶子结点。 重要性质:交换了一个点的左右子树之后,不会影响左子树内和右子树内的逆序对数量。 考虑一个任意的结点,对它的子树中叶子的逆序对进行分类讨论: 都在左子树内; 都在右子树内; 跨越左右子树。 交换左右子树之后,受到影响的显然只有第三种情况。第一、第二种情况分治下去就可以转化成第三种再计算。 如何计算答案呢?一开始,对于每一个叶子结点,我们都建立一棵权值线段树(动态开点)并记录 $p_i$ 出现了 $1$ 次。合并 $r1, r2$ 时,逆序对的个数就是 $tree[rc[r1]] \times tree[lc[r2]]$。因为 $r1, r2$ 对应的权值区间 $[L, R]$ 是相同的,而 $lc[r1], lc[r2]$ 对应的权值区间就是 $[L, M]$, $rc[r1], rc[r2]$ 对应的权值区间是 $[M+1, R]$,$rc[]$ 中记录的数都比 $lc[]$ 中的要大,而 $r1$ 对应的数的编号小于 $r2$(也就是 $r1$ 是左子树,$r2$ 是右子树),满足逆序对的条件。 这时算出来的逆序对的个数是不交换左右子树的情况,我们只要再计算出交换左右子树的逆序对的个数 $tree[rc[r2]] \times tree[lc[r1]]$,进行比较,取较小值为当前层级的答案即可。 RECORD 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 #include <bits/stdc++.h> using namespace std; #define LL long long const int MAXN = 200005; int n; LL ans = 0, u = 0, v = 0; int cnt = 0, roc = 0, root[MAXN << 5], lc[MAXN << 5], rc[MAXN << 5], tree[MAXN << 5]; int build(int l, int r, int p) { int pos = ++cnt; if (l == r) { tree[pos] = 1; return pos; } int m = (l + r) >> 1; if (p <= m) lc[pos] = build(l, m, p); else rc[pos] = build(m + 1, r, p); tree[pos] = tree[lc[pos]] + tree[rc[pos]]; return pos; } int merge(int r1, int r2, int l, int r) { if (!r1 || !r2) return r1 ^ r2; if (l == r) { tree[r1] += tree[r2]; return r1; } int m = (l + r) >> 1; u += (LL)tree[rc[r1]] * tree[lc[r2]]; v += (LL)tree[rc[r2]] * tree[lc[r1]]; lc[r1] = merge(lc[r1], lc[r2], l, m); rc[r1] = merge(rc[r1], rc[r2], m + 1, r); tree[r1] = tree[lc[r1]] + tree[rc[r1]]; return r1; } int dfs() { int x, pos; cin >> x; if (x == 0) { int ls = dfs(), rs = dfs(); u = v = 0; pos = merge(ls, rs, 1, n); ans += min(u, v); } else { pos = build(1, n, x); } return pos; } int main() { cin >> n; dfs(); cout << ans << endl; return 0; }

2022/8/14
articleCard.readMore

可持久化线段树笔记

动态开点线段树 常规写法的线段树只能维护不算很长的数组,由于空间不够,对于 $10^9$ 级别的数组却不能很好地维护。所以,我们要用到动态开点线段树。 核心思想:节点只有在有需要的时候才被创建。 比如说,要求在一个长度为 $n < 10^9$ 的数组上实现区间求和、单点修改的操作,初始数组元素值均为 0。 那么,我们一开始只创建一个根结点,接下来遵循动态开点的核心思想进行操作。 比如下面这张图的例子,我们依次修改 1, 2, 8 三个结点,途中创建了必要的结点。而在图中没有显示的空结点并没有被创建,视为 0,这样就节省了空间。 那么对于区间修改时,会有 pushdown() 操作,可能会修改一个不存在的结点。这时有两个解决方案: 在 pushdown() 时,如果缺少孩子,就直接创建一个新的孩子就可以了。 使用 标记永久化 技巧(李超线段树),让结点不再进行 pushdown(),进一步节省了空间。 复杂度分析:单次操作的时间复杂度是不变的,为 $O(\log n)$。对于空间复杂度,由于每次操作都有可能创建并访问全新的一系列结点,因此 $m$ 次操作的空间复杂度是 $O(m\log n)$,不再是原本线段树的 $O(n)$。 代码实现: 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 int n, cnt, root; // cnt 表示当前结点个数 int sum[N*2], ls[N*2], rs[N*2]; void upd(int& rt, int l, int r, int p, int f) { // 注意这里传入一个引用,可以修改 ls 或 rs 数组 if (!rt) rt = ++cnt; // 当结点为空时,创建一个新的结点 if (l == r) { sum[rt] += f; return; } int m = (l + r) >> 1; if (p <= m) upd(ls[rt], l, m, p, f); else upd(rs[rt], m + 1, r, p, f); sum[rt] = sum[ls[rt]] + sum[rs[rt]]; // pushup } int query(int rt, int l, int r, int L, int R) { if (!rt) return 0; // 如果结点为空,返回 0 if (l >= L && r <= R) return sum[rt]; int m = (l + r) >> 1, ans = 0; if (L <= m) ans += query(ls[rt], l, m, L, R); if (R > m) ans += query(rs[rt], m + 1, r, L, R); return ans; } 可持久化线段树,就是可以存储历史信息的线段树。 比如我们对数组进行了 n 次修改,然后突然希望回到第 i 个版本,然后又基于这个版本进行一些新的修改等,就是可持久化线段树需要解决的问题。 最暴力的想法是,对于每一次修改,都重新建一棵完整的线段树。然后,空间爆炸了。 我们分析一下,就会发现每一次修改时,改变的结点的个数都是线段树的深度 $\log n$。如图,我们修改了结点 $8$ 的值,受到影响的结点都在从根结点 $1$ 到 $8$ 的路径上,即 $1, 2, 4, 8$。其他的结点都是不变的。因此,我们只需要重新创建有改动的结点即可,例如 $1(new), 2(new), 4(new), 8(new)$(新的结点是动态开点的,编号和原来的不一样,因此这里用 $(new)$ 标注)。然后,例如对于新的根结点 $1(new)$,将左儿子指向新的 $2(new)$,右儿子和旧的根结点 $1(old)$ 的右儿子相同,是 $3(old)$,不用修改。 Luogu-P3919 【模板】可持久化线段树 1(可持久化数组) 如题,你需要维护这样的一个长度为 $ N $ 的数组,支持如下几种操作 在某个历史版本上修改某一个位置上的值 访问某个历史版本上的某一位置的值 此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组) $ 1 \leq N, M \leq {10}^6, 1 \leq {loc}_i \leq N, 0 \leq v_i < i, -{10}^9 \leq a_i, {value}_i \leq {10}^9$ 程序实现: 为了保险,线段树数组的大小应设为 n << 5。洛谷上关于此的讨论。 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 #include <bits/stdc++.h> using namespace std; const int MAXN = 1000006; int n, m; int a[MAXN]; int rt[MAXN << 5], lc[MAXN << 5], rc[MAXN << 5], sm[MAXN << 5], ver = 0, tot = 0; void build(int& rt, int l, int r) { rt = ++tot; if (l == r) { sm[rt] = a[l]; return; } int m = (l + r) >> 1; build(lc[rt], l, m); build(rc[rt], m + 1, r); } void update(int& rt, int pre, int l, int r, int q, int v) { // pre:原结点 rt = ++tot; lc[rt] = lc[pre]; rc[rt] = rc[pre]; sm[rt] = sm[pre]; // 复制原结点的信息,之后按需修改 if (l == r) { sm[rt] = v; return; } int m = (l + r) >> 1; if (q <= m) update(lc[rt], lc[pre], l, m, q, v); // 新建左子结点,但是右子结点不变 else update(rc[rt], rc[pre], m + 1, r, q, v); // 反之 } int query(int rt, int l, int r, int q) { if (l == r) return sm[rt]; int m = (l + r) >> 1; if (q <= m) return query(lc[rt], l, m, q); return query(rc[rt], m + 1, r, q); } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) cin >> a[i]; build(rt[0], 1, n); while (m--) { int v, opt, loc, val; cin >> v >> opt >> loc; if (opt == 1) { cin >> val; update(rt[++ver], rt[v], 1, n, loc, val); // rt[++ver] 以 rt[v] 为基础 } else { cout << query(rt[v], 1, n, loc) << endl; rt[++ver] = rt[v]; } } return 0; } 普通的线段树维护的是区间内最值或总和,而权值线段树每个结点维护的是在一个区间内的数出现的次数。因此,可以把权值线段树看作一个桶。举个例子,如果 $tree[1]$ 对应的区间是 $[1, 8]$,那么它的值就是 $1, 2, \cdots, 7, 8$ 在原数组中出现的次数之和。 权值线段树中,采用元素到下标数组映射的方式进行插入。当数据离散程度较大的情况,空间的利用效率比较低,这时候可以搭配离散化技巧(下面有例子)。 一个常见的应用是把权值线段树用于处理区间第 $K$ 小/大问题。 例题:Luogu-P1138 第 k 小整数 RECORD 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 <algorithm> #include <cstdio> #include <iostream> using namespace std; const int MAXN = 10005; int n, k, a[MAXN]; int tree[MAXN << 1], lc[MAXN << 1], rc[MAXN << 1], root = 0, tot = 0; // 对于权值线段树来说,build 函数是没有必要的,因为默认值为 0 // 只不过这里为了节省空间用了动态开点,才需要 build() void build(int &rt, int l, int r) { rt = ++tot; if (l == r) return; int m = (l + r) >> 1; build(lc[rt], l, m); build(rc[rt], m + 1, r); } void update(int rt, int l, int r, int q) { if (l == r) { tree[rt] = 1; // 出现了 1 次 return; } int m = (l + r) >> 1; if (q <= m) update(lc[rt], l, m, q); else update(rc[rt], m + 1, r, q); tree[rt] = tree[lc[rt]] + tree[rc[rt]]; // 区间内数的数量 } int kth(int rt, int l, int r, int k) { if (l == r) return l; int m = (l + r) >> 1; if (k > tree[lc[rt]]) return kth(rc[rt], m + 1, r, k - tree[lc[rt]]); // 在右半区间内 return kth(lc[rt], l, m, k); // 在左半区间内 } int main() { cin >> n >> k; build(root, 1, n); for (int i = 1; i <= n; i++) cin >> a[i]; sort(a + 1, a + n + 1); // 离散化+去重 n = unique(a + 1, a + 1 + n) - a - 1; // 记得 -1 if (k > n) { cout << "NO RESULT\n"; return 0; } for (int i = 1; i <= n; i++) { update(root, 1, n, i); } // 离散化后,插入下标 cout << a[kth(root, 1, n, k)] << endl; // 离散化后,kth() 查到的是第 k 小整数的 **下标** return 0; } 注意:离散化时用 unique() 最后面一定要 -1!!! 主席树名称来源于其发明人黄嘉泰的姓名的首字母缩写 HJT。 Luogu-P3834 【模板】可持久化线段树 2 如题,给定 $n$ 个整数构成的序列 $a$,将对于指定的闭区间 $[l, r]$ 查询其区间内的第 $k$ 小值。 $1 \leq n,m \leq 2\times 10^5$,$|a_i| \leq 10^9$,$1 \leq l \leq r \leq n$,$1 \leq k \leq r - l + 1$。 首先看到第 $k$ 小问题,想到要使用权值线段树。 对于每一个数,我们都 我们先把问题简化一下:每次求 $[1, r]$ 区间内的 $k$ 小值。 这时,只要找到插入 $r$ 的版本的权值线段树,然后查找即可。 回到原问题:求 $[l, r]$ 区间内的 $k$ 小值。 我们发现,主席树其实具有前缀和的性质,查询 $[l, r]$ 可以转化为 $[1, r]$ 减去 $[1, l-1]$ 的对应区间的大小。 注意数据范围,$2\times 10^5$ 个数,$|a_i| \leq 10^9$,因此用离散化。 RECORD 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 #include <bits/stdc++.h> using namespace std; const int MAXN = 200005; int n, m, q; int a[MAXN], b[MAXN]; int rt[MAXN << 5], lc[MAXN << 5], rc[MAXN << 5], sm[MAXN << 5], tot = 0; void build(int& rt, int l, int r) { rt = ++tot; if (l == r) return; int m = (l + r) >> 1; build(lc[rt], l, m); build(rc[rt], m + 1, r); } void update(int& rt, int pre, int l, int r, int q) { rt = ++tot; // 动态开点 lc[rt] = lc[pre]; rc[rt] = rc[pre]; sm[rt] = sm[pre] + 1; if (l == r) return; int m = (l + r) >> 1; if (q <= m) update(lc[rt], lc[pre], l, m, q); else update(rc[rt], rc[pre], m + 1, r, q); } int query(int rt, int pre, int l, int r, int k) { if (l == r) return l; int x = sm[lc[rt]] - sm[lc[pre]]; // 对应区间相减 int m = (l + r) >> 1; if (x >= k) return query(lc[rt], lc[pre], l, m, k); return query(rc[rt], rc[pre], m + 1, r, k - x); } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); b[i] = a[i]; } // 离散化 sort(b + 1, b + 1 + n); q = unique(b + 1, b + 1 + n) - b - 1; build(rt[0], 1, q); for (int i = 1; i <= n; i++) { int pos = lower_bound(b + 1, b + 1 + q, a[i]) - b; update(rt[i], rt[i - 1], 1, q, pos); } while (m--) { int l, r, k; scanf("%d%d%d", &l, &r, &k); printf("%d\n", b[query(rt[r], rt[l - 1], 1, q, k)]); } return 0; } 算法学习笔记(49): 线段树的拓展 - Pecco 算法学习笔记(50): 可持久化线段树 - Pecco 数据结构 线段树–权值线段树 详解 - HeartFireY

2022/8/14
articleCard.readMore

线段树合并笔记

前置知识:动态开点线段树。 合并是一个递归的过程。首先合并两棵以 $u, v$ 为根的二叉树: 考虑左子树 如果 $u, v$ 都没有左子树,那么直接留空; 如果只有 $u$ 有左子树,那么 $u$ 的左子树保留不动; 如果只有 $v$ 有左子树,那么将 $v$ 的左子树接过来,成为 $u$ 的左子树; 如果 $u, v$ 均有左子树,那么递归合并 $u, v$ 的左子树,结果赋给 $u$ 的左子树。 考虑右子树 如果 $u, v$ 都没有右子树,那么直接留空; 如果只有 $u$ 有右子树,那么 $u$ 的右子树保留不动; 如果只有 $v$ 有右子树,那么将 $v$ 的右子树接过来,成为 $u$ 的右子树; 如果 $u, v$ 均有右子树,那么递归合并 $u, v$ 的右子树,结果赋给 $u$ 的右子树。 最后我们就将两棵二叉树合并成了一个以 $u$ 为根的二叉树。 复杂度分析:在上面的过程中,仅当 $u, v$ 均有左(右)孩子时才会进行递归,访问这个左(右)孩子。时间复杂度就是两棵二叉树中重复的结点的数量。 程序实现: 指针实现如下: 1 2 3 4 5 6 7 8 TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) { if (!root1 && !root2) return NULL; // 事实上这一个判断略显多余,仅保留后面两行也可以 if (!root1) return root2; if (!root2) return root1; root1->left = mergeTrees(root1->left, root2->left); root1->right = mergeTrees(root1->right, root2->right); return root1; } 数组实现: 1 2 3 4 5 6 int mergeTrees(int root1, int root2) { if (!root1 || !root2) return root1 ^ root2; // 非常简便的写法 lc[root1] = mergeTrees(lc[root1], lc[root2]); rc[root1] = mergeTrees(rc[root1], rc[root2]); return root1; } 这种写法是直接在原来一棵树的基础上修改,还有一种写法是动态开点,形成一个全新的线段树。 线段树本质上也是二叉树,因此合并线段树和合并二叉树是差不多的。不同的是,线段树每个结点还维护了其他的信息,例如区间最大值,总和等等。 我们知道,线段树上一个结点的信息需要从它的子结点中得出,因此只要在合并完一个结点的左子树和右子树之后,将左右儿子结点的信息取出来维护即可。叶子结点就更加简单了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void up(int root) { maxx[root] = max(maxx[lc[root]], maxx[rc[root]]); sum[root] = sum[lc[root]] + sum[rc[root]]; } int merge(int root1, int root2, int l, int r) { if (!root1 || !root2) return root1 ^ root2; if (l == r) { // 处理叶子节点 tree[root1] += tree[root2]; return root1; } int m = (l + r) >> 1; lc[root1] = merge(lc[root1], lc[root2], l, m); rc[root1] = merge(rc[root1], rc[root2], m + 1, r); up(root1); return root1; } 我们在做线段树区间修改时,通常会使用懒标记(lazy tag)。一旦向下访问,当前结点的标记就要下放(pushdown)。但当我们要支持线段树合并时,两棵树的标记该怎么处理呢? 关键点在于:一棵线段树上做的标记,只代表对这个线段树的结点的修改,与另一棵无关。因此,在合并这两个结点之前,先将这两个标记下放。 Luogu-P3224 「HNOI2012」永无乡 永无乡包含 $n$ 座岛,编号从 $1$ 到 $n$ ,每座岛都有自己的独一无二的重要度,按照重要度可以将这 $n$ 座岛排名,名次用 $1$ 到 $n$ 来表示。某些岛之间由巨大的桥连接,通过桥可以从一个岛到达另一个岛。如果从岛 $a$ 出发经过若干座(含 $0$ 座)桥可以 到达岛 $b$ ,则称岛 $a$ 和岛 $b$ 是连通的。 现在有两种操作: B x y 表示在岛 $x$ 与岛 $y$ 之间修建一座新桥。 Q x k 表示询问当前与岛 $x$ 连通的所有岛中第 $k$ 重要的是哪座岛,即所有与岛 $x$ 连通的岛中重要度排名第 $k$ 小的岛是哪座,请你输出那个岛的编号。 $1 \leq m \leq n \leq 10^5$, $1 \leq q \leq 3 \times 10^5$,$p_i$ 为一个 $1 \sim n$ 的排列,$op \in {\texttt Q, \texttt B}$,$1 \leq u, v, x, y \leq n$。 先只看询问操作,实际上是一个第 k 大问题,可以用权值线段树解决。 通过建桥,我们可以得到几个连通块,同时还要合并操作。很容易联想到 并查集。 在这个过程中,每一个连通块都应拥有一棵权值线段树,所以一开始对于每一个岛都建立一个权值线段树,然后动态开点这座岛的重要度。建桥时,先进行线段树合并,再进行并查集合并。 注意合并过程中,线段树合并的方向要与并查集合并的方向一致。例如 fa[a] = b;,那么 root[b] 就要指向线段树合并的根。或者为了保险,将 root[a] 和 root[b] 都指向新的根也是没问题的。 RECORD 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 #include <bits/stdc++.h> using namespace std; const int MAXN = 300005; int n, m; int p[MAXN], ans[MAXN]; int root[MAXN << 5], lc[MAXN << 5], rc[MAXN << 5], tree[MAXN << 5], cnt = 0; int fa[MAXN]; int find(int x) { if (fa[x] == x) return x; return fa[x] = find(fa[x]); } int bcmerge(int a, int b) { a = find(a), b = find(b); fa[a] = b; } void up(int u) { tree[u] = tree[lc[u]] + tree[rc[u]]; } void ins(int &rt, int l, int r, int p) { if (!rt) rt = ++cnt; if (l == r) { tree[rt] = 1; return; } int m = (l + r) >> 1; if (p <= m) ins(lc[rt], l, m, p); else ins(rc[rt], m + 1, r, p); up(rt); } int merge(int u, int v, int l, int r) { if (!u || !v) return u ^ v; if (l == r) { tree[u] += tree[v]; return u; } int m = (l + r) >> 1; lc[u] = merge(lc[u], lc[v], l, m); rc[u] = merge(rc[u], rc[v], m + 1, r); up(u); return u; } int kth(int u, int l, int r, int k) { if (l == r) return l; int m = (l + r) >> 1; if (k > tree[lc[u]]) return kth(rc[u], m + 1, r, k - tree[lc[u]]); return kth(lc[u], l, m, k); } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> p[i]; ans[p[i]] = i; fa[i] = i; ins(root[i], 1, n, p[i]); } while (m--) { int u, v; cin >> u >> v; root[find(v)] = merge(root[find(u)], root[find(v)], 1, n); bcmerge(u, v); } int q; cin >> q; while (q--) { char op; int x, y; cin >> op >> x >> y; if (op == 'Q') { if (tree[root[find(x)]] < y) cout << -1 << endl; else cout << ans[kth(root[find(x)], 1, n, y)] << endl; } else { root[find(y)] = merge(root[find(x)], root[find(y)], 1, n); bcmerge(x, y); } } return 0; } 线段树合并:从入门到精通

2022/8/14
articleCard.readMore

Luogu-P1776 宝物筛选

Luogu-P1776 宝物筛选 终于,破解了千年的难题。小 FF 找到了王室的宝物室,里面堆满了无数价值连城的宝物。 这下小 FF 可发财了,嘎嘎。但是这里的宝物实在是太多了,小 FF 的采集车似乎装不下那么多宝物。看来小 FF 只能含泪舍弃其中的一部分宝物了。 小 FF 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,之后开始了宝物筛选工作:小 FF 有一个最大载重为 $W$ 的采集车,洞穴里总共有 $n$ 种宝物,每种宝物的价值为 $v_i$,重量为 $w_i$,每种宝物有 $m_i$ 件。小 FF 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。 对于 $100%$ 的数据,$n\leq \sum m_i \leq 10^5$,$0\le W\leq 4\times 10^4$,$1\leq n\le 100$。 每一个数都可以表示成 $2$ 的幂的和(因为每一个数都可以用二进制表示)。 时间复杂度:$O(nW\sum \log m_i)$ 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 #include <bits/stdc++.h> using namespace std; int dp[40005]; int main() { int n, W; scanf("%d%d", &n, &W); for (int i = 0; i < n; i++) { int t, c, p; scanf("%d%d%d", &c, &t, &p); int tmp = 1; while (p >= tmp) { p -= tmp; for (int j = W; j >= t*tmp; j--) dp[j] = max(dp[j], dp[j-t*tmp]+c*tmp); tmp *= 2; } if (p == 0) continue; for (int j = W; j >= t*p; j--) { dp[j] = max(dp[j], dp[j-t*p]+c*p); } } printf("%d\n", dp[W]); return 0; } 设 $f[i][j]$ 表示前 $i$ 种物品重量不超过 $j$ 的最大价值。显然有一个状态转移方程: $$ f[i][j] = max_{0<=k<=m[i]}{f[i-1][j-k\times w[i]]+k\times v[i]} $$ 我们设 $j=k_1*w[i]+d$: $$ f[i][j]=max{f[i-1][(k_1-k)\times w[i]+d]-(k_1-k)\times v[i] }+ k_1\times v[i] $$ 我们枚举余数 $d$ 和 $k_1$,就可以表示出每一个数,然后维护一个单调队列,这个队列里面的决策通过加上第 $i$ 种物品都可以凑成 $j$,因此增加的数量不能超过 $m[i]$,也就是 $k_1-k <= m[i]$。 时间复杂度:$O(nW)$ RECORD 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 #include <bits/stdc++.h> using namespace std; int n, W; int v[105], w[105], m[105]; int dp[105][40004]; struct deq { int head, tail; int arr[40004]; bool empty() { return head+1 == tail; } void pop_front() { head++; } void pop_back() { tail--; } void push_back(int x) { arr[tail] = x; tail++; } void clear() { head = 0; tail = 1; } int front() { return arr[head+1]; } int back() { return arr[tail-1]; } } dq; int main() { cin >> n >> W; int ans = 0; for (int i = 1; i <= n; i++) { cin >> v[i] >> w[i] >> m[i]; if (w[i] == 0) { ans += v[i]*m[i]; continue; } for (int j = 0; j < w[i]; j++) { // 余数 dq.clear(); // k 实际上表示的是文中的 k_1 for (int k = 0; j+k*w[i] <= W; k++) { // dq.front() 实际上表示的是文中的 k_1 - k while (!dq.empty() && k-dq.front() > m[i]) dq.pop_front(); while (!dq.empty() && dp[i-1][j+dq.back()*w[i]]+(k-dq.back())*v[i] <= dp[i-1][j+k*w[i]]) dq.pop_back(); dq.push_back(k); if (!dq.empty()) dp[i][j+k*w[i]] = max(dp[i][j+k*w[i]], dp[i-1][j+dq.front()*w[i]]+(k-dq.front())*v[i]); } } } cout << dp[n][W]+ans << endl; return 0; }

2022/8/12
articleCard.readMore

Luogu-P2254 「NOI2005」瑰丽华尔兹

「NOI2005」瑰丽华尔兹 不妨认为舞厅是一个 $N$ 行 $M$ 列的矩阵,矩阵中的某些方格上堆放了一些家具,其他的则是空地。钢琴可以在空地上滑动,但不能撞上家具或滑出舞厅,否则会损坏钢琴和家具,引来难缠的船长。每个时刻,钢琴都会随着船体倾斜的方向向相邻的方格滑动一格,相邻的方格可以是向东、向西、向南或向北的。而艾米丽可以选择施魔法或不施魔法:如果不施魔法,则钢琴会滑动;如果施魔法,则钢琴会原地不动。 艾米丽是个天使,她知道每段时间的船体的倾斜情况。她想使钢琴在舞厅里滑行的路程尽量长,这样 1900 会非常高兴,同时也有利于治疗托尼的晕船。但艾米丽还太小,不会算,所以希望你能帮助她。 $100%$ 的数据中,$1\leq N, M \leq 200$,$K \leq 200$,$T\leq 40000$。 首先我们定义一下状态。设 $dp[t][i][j]$ 表示 t 时刻,在 $(i, j)$ 滑行的最长路程长度。状态转移方程是 $dp[t][i][j] = \max(dp[t-1][i][j], dp[t-1][i^{’}][j^{’}])$,$i^{’}$ 和 $j^{’}$ 是合法的走过来的位置,取决于 $t$ 时刻船体倾斜的方向。 这样设计状态的话,时间复杂度为 $O(TNM)$,空间似乎也是问题。 这时候我们发现还有一个变量 $K$ 没有用到,考虑把状态设为 $dp[t][i][j]$ 表示在第 $t$ 时间段内,在 $(i, j)$ 滑行的最长路程长度。时间复杂度为 $O(KN^3)$,暂时还不行。 我们看看能不能进一步优化。下面假设当前船体倾斜的方向是东。设当前时间段长度是 $tim$,上一个时间段钢琴的位置在 $(i, m)$,那么 $dp[t][i][j] = \max_{j-m<=tim}{dp[t-1][i][m]+j-m}$。在这个式子中,只有 $m$ 一个变量,并且 $j-m<=tim$,合法决策在一段相邻区间内,可以用到单调队列优化! 对于两个决策 $m_1 < m_2$,$m_2$ 优于 $m_1$ 时仅当: $$ \begin{aligned} dp[t-1][i][m_1]+(j-m_1) & < dp[t-1][i][m_2]+(j-m_2) \\ dp[t-1][i][m+1]+m_2-m_1 & < dp[t-1][i][m_2] \end{aligned} $$ 在单调队列向尾部加入新决策时,我们就用这个公式来判断队尾需不需要出队,维护单调性。 这样时间复杂度降到了 $KN^2$。 注意细节: 不合法($-1$)的决策不要入队 RECORD 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 #include <bits/stdc++.h> using namespace std; int n, m, x, y, k; char a[205][205]; int s[205], t[205], d[205]; int dp[205][205][205]; struct deq { int head, tail; int arr[20005]; bool empty() { return head + 1 == tail; } void pop_front() { head++; } void pop_back() { tail--; } void push_back(int x) { arr[tail] = x; tail++; } void clear() { head = 0; tail = 1; } int front() { return arr[head + 1]; } int back() { return arr[tail - 1]; } } q; int main() { cin >> n >> m >> x >> y >> k; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) cin >> a[i][j]; memset(dp, -1, sizeof(dp)); dp[0][x][y] = 0; int ans = 1; for (int p = 1; p <= k; p++) { cin >> s[p] >> t[p] >> d[p]; int tim = t[p] - s[p] + 1; // 下面进行对四个方向的分类讨论 if (d[p] == 1) { for (int j = 1; j <= m; j++) { q.clear(); for (int i = n; i >= 1; i--) { if (a[i][j] == 'x') { q.clear(); continue; } dp[p][i][j] = dp[p - 1][i][j]; while (!q.empty() && q.front() - i > tim) q.pop_front(); if (!q.empty()) { dp[p][i][j] = max(dp[p][i][j], dp[p - 1][q.front()][j] + q.front() - i); ans = max(ans, dp[p][i][j]); } while (!q.empty() && dp[p - 1][i][j] - dp[p - 1][q.back()][j] >= q.back() - i) q.pop_back(); if (dp[p - 1][i][j] != -1) q.push_back(i); } } } else if (d[p] == 2) { for (int j = 1; j <= m; j++) { q.clear(); for (int i = 1; i <= n; i++) { if (a[i][j] == 'x') { q.clear(); continue; } dp[p][i][j] = dp[p - 1][i][j]; while (!q.empty() && i - q.front() > tim) q.pop_front(); if (!q.empty()) { dp[p][i][j] = max(dp[p][i][j], dp[p - 1][q.front()][j] + i - q.front()); ans = max(ans, dp[p][i][j]); } while (!q.empty() && dp[p - 1][i][j] - dp[p - 1][q.back()][j] >= i - q.back()) q.pop_back(); if (dp[p - 1][i][j] != -1) q.push_back(i); } } } else if (d[p] == 3) { for (int i = 1; i <= n; i++) { q.clear(); for (int j = m; j >= 1; j--) { if (a[i][j] == 'x') { q.clear(); continue; } dp[p][i][j] = dp[p - 1][i][j]; while (!q.empty() && q.front() - j > tim) q.pop_front(); if (!q.empty()) { dp[p][i][j] = max(dp[p][i][j], dp[p - 1][i][q.front()] + q.front() - j); ans = max(ans, dp[p][i][j]); } while (!q.empty() && dp[p - 1][i][j] - dp[p - 1][i][q.back()] >= q.back() - j) q.pop_back(); if (dp[p - 1][i][j] != -1) q.push_back(j); } } } else { for (int i = 1; i <= n; i++) { q.clear(); for (int j = 1; j <= m; j++) { if (a[i][j] == 'x') { q.clear(); continue; } dp[p][i][j] = dp[p - 1][i][j]; while (!q.empty() && j - q.front() > tim) q.pop_front(); if (!q.empty()) { dp[p][i][j] = max(dp[p][i][j], dp[p - 1][i][q.front()] + j - q.front()); ans = max(ans, dp[p][i][j]); } while (!q.empty() && dp[p - 1][i][j] - dp[p - 1][i][q.back()] >= j - q.back()) q.pop_back(); if (dp[p - 1][i][j] != -1) q.push_back(j); } } } } cout << ans << endl; return 0; }

2022/8/12
articleCard.readMore

点分治笔记

点分治,外国人称之为 Centroid decomposition,重心分解。 学习重心分解之前,自然要先了解重心。 下面统一用 $n$ 表示树上结点的个数。 在一棵树中,如果删除一个顶点后得到的最大子树的顶点数最少,那么这个点就是树的重心(Centroid)。 重心的性质: 删除重心后得到的所有子树,其顶点数必然不超过 $n/2$。 证明:选取任意顶点作为起点,每次都沿着边向最大子树的方向移动,最终一定会到达某个顶点,将其删除后得到的所有子树的顶点数都不超过 $n/2$。如果这样的点存在的话,那么也就可以证明删除重心后得到的所有子树的顶点数都不超过 $n/2$。 记当前顶点为 $v$,如果顶点 $v$ 已经满足上述条件则停止。否则,与顶点 $v$ 邻接的某个子树的顶点数必然大于 $n/2$。假设顶点 $v$ 与该子树中的顶点 $w$ 邻接,那么我们就把顶点 $w$ 作为新的顶点 $v$。不断重复这一步骤,必然会在有限步停止。这是因为对于移动中所用的边 $(v, w)$,必有 $v$ 侧的子树的顶点数小于 $n/2$,$w$ 侧的子树的顶点数大于 $n/2$,所以不可能再从 $w$ 移动到 $v$。因而该操作永远不会回到已经经过的顶点,而顶点数又是有限的,所以算法必然在有限步终止。 树中所有顶点到某个顶点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。 把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。 在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。 更多证明请见:树的重心的性质及其证明 - suxxsfe - 博客园 (cnblogs.com) 根据重心的定义,先以 $1$ 为根进行 DFS。在递归中计算子树大小 $siz[u]$,并求出最大的子树的大小 $maxs[u]$,比较出重心 $centroid$。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void getCentroid(int u, int fa, int s) {   siz[u] = 1;   maxs[u] = 0;   for (int i = head[u]; i != 0; i = nxt[i]) {     if (to[i] == fa)       continue;     getCentroid(to[i], u, s);     siz[u] += siz[to[i]];     maxs[u] = max(maxs[u], siz[to[i]]);   } // “向上” 的部分也是该结点的子树   maxs[u] = max(maxs[u], s - siz[u]);   if (maxs[u] < maxs[centroid] || !centroid) centroid = u; } int main() {   centroid = 0;   getCentroid(1, 0, n);   return 0; } 提交:CSES - Finding a Centroid 我们可以用点分治解决关于统计树上路径的问题。 Luogu P3806【模板】点分治 1:给定一棵有 $n$ 个点的带边权树,$m$ 次询问,每次询问给出 $k$,询问树上距离为 $k$ 的点对是否存在。 $n\le 10000,m\le 100,k\le 10000000$ 暴力的做法至少需要 $O(n^{2})$,显然会超时,所以考虑分治。 如何分割呢?如果随意选择顶点的话,递归的深度可能退化成 $O(n)$。若我们每次选择子树的重心作为新的根结点,可以让递归的层数最少。因为每次树的大小至少减半,所以递归的深度是 $O(log n)$。 设当前的根结点是 $rt$。 对于每一条路径 $(u, v)$,必然满足以下三种情况之一: 顶点 $u, v$ 在 $rt$ 的同一子树内。 顶点 $u, v$ 分别在 $rt$ 的不同子树内。 顶点 $u, v$ 其中一个是 $rt$。 对于第 (1) 种情况,可以递归后转化成另外的情况。对于第 (2) 种情况,从顶点 $u$ 到顶点 $v$ 的路径必然经过根结点 $rt$,只要求出每个顶点到 $rt$ 的距离,就可以统计出答案。对于第 (3) 种情况,可以添加一个到 $rt$ 距离为 $0$ 的顶点,就转化为了第 (2) 种情况。 需要注意的是,在第 (1) 种情况中统计的同一子树的顶点对,要避免在第 (2) 种情况中被重复统计。通过容斥和类似于树上背包的方法可以去重。 最后的时间复杂度是 $O(nlog^{2}n)$。 RECORD。 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 int n, m; int L, p; int centroid; bool vis[MAXN], cnt[MAXW], ans[MAXM]; int ask[MAXM]; vector< std::pair< int, int > > g[MAXN]; int dis[MAXN]; int siz[MAXN], maxs[MAXN]; void getCentroid(int u, int fa, int s) { siz[u] = 1; maxs[u] = 0; for (auto i : g[u]) { if (i.first == fa || vis[i.first]) continue; getCentroid(i.first, u, s); siz[u] += siz[i.first]; maxs[u] = std::max(maxs[u], siz[i.first]); } maxs[u] = std::max(maxs[u], s - siz[u]); if (maxs[u] < maxs[centroid] || !centroid) centroid = u; } // 获取子树中所有点到重心的距离 void getDis(int u, int fa, int d) { dis[p++] = d; for (auto i : g[u]) { if (i.first == fa || vis[i.first]) continue; getDis(i.first, u, d + i.second); } } void calc() { L = p = 0; cnt[0] = 1; // 类似于树上背包 for (auto i : g[centroid]) { if (vis[i.first]) continue; getDis(i.first, centroid, i.second); // 一棵一棵子树合并,不会重复统计 for (int i = L; i < p; i++) { for (int j = 0; j < m; j++) { if (dis[i] > ask[j]) continue; ans[j] |= cnt[ask[j] - dis[i]]; } } for (int j = L; j < p; j++) cnt[dis[j]] = 1; L = p; } // 还原 cnt 数组 for (int i = 0; i < p; i++) cnt[dis[i]] = 0; // 不能用 memset } void solve(int u, int size) { centroid = 0; getCentroid(u, -1, size); getCentroid(centroid, -1, size); // 再求一次 siz,防止后面找重心时出错 vis[centroid] = true; calc(); for (auto i : g[centroid]) { if (vis[i.first]) continue; solve(i.first, siz[i.first]); } } int main() { solve(1, n); for (int i = 0; i < m; i++) { if (ans[i]) cout << "AYE\n"; else cout << "NAY\n"; } return 0; } 需要注意的细节: 分治的时候求出新的重心之后,也要再次求子树 $siz$。 详见:一种基于错误的寻找重心方法的点分治的复杂度分析 - 博客 - liu_cheng_ao的博客 (uoj.ac) 账号 920848348 评论: 对于大部分点分治的代码中,不能直接 size = siz[v],否则会导致后面的重心求错(可以用下面的样例试一下,然后输出重心节点看一下,你会发现从 $6$->$3$ 这一大块的重心应该是 $2$,而代码输出是 $1$。原因在于: 由于一开始从 点 $1$ 开始找整棵树的重心,此时的 $siz[u]$ 表示的仅仅是以 1 为根结点的树中,$u$ 的子树大小。那么现在重心 $root$ 找到了,那么从这整棵树的重心 $root$ 开始深搜时,若有边 $root$->$v$ ,而此时的 $siz[v]$ 的值并不是以 $v$ 为根的子树的大小(这个子树当然不包括父亲 $root$ 那一块)。具体为什么,这里给一组样例,大家可以在图中画一下。 此样例的对应题目为 Luogu P3806【模板】点分治 1。 然后我们一步一步来: 先从 $1$ 开始 dfs ,统计 $siz$ 数组,此时很清晰的知道 $siz[3] = 7$ ,因为是以 $1$ 为根深搜的。很明显,一开始这整棵树的重心是 6 号节点,那么接下来,我们可以发现: 当从点 $6$ 开始 dfs 时,有 $6$->$3$ 这条边,那么按理来说,$size = siz[3]$,此时 $size$ 表示的是以 $3$ 为根的子树的大小,可看图上明明是 $5$ 啊(在点 $3$ 的左边,不包含点 $6$ 那边)。可是此时的 $siz[3]$ 为 $7$,而并非是 $5$ 。这是由于选定的深搜节点不同,统计的不同而导致的。$siz[3]$ 的正确值理应来自于从 点 $6$ 开始深搜的值。 故我们可以得出结论,用 getroot(1,0) 找到整棵树的重心之后,再来一次 getroot(root, 0),来确定以重心为根结点时的 $siz$ 数组。这下就可以直接 size = siz[v] 了。 11 1 6 7 1 6 8 1 7 9 1 7 10 1 8 11 1 1 2 1 1 3 1 2 4 1 2 5 1 3 6 1 2 不要用 memset 粗暴还原,会浪费很多时间。 Luogu P4178 Tree:给定一棵有 $n$ 个点的带权树,给出 $k$,询问树上距离小于等于 $k$ 的点对数量。 $n\le 40000,k\le 20000,w_i\le 1000$ 这题方法比较多。下面的代码用 容斥 进行去重和 双指针 (除此之外还可以用二分)统计答案。 RECORD。 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 int calc(int u, int w) { dis.clear(); getDis(u, -1, w); sort(dis.begin(), dis.end()); int sum = 0; int L = 0, R = dis.size() - 1; // 双指针 while (L < R) { if (dis[L] + dis[R] <= k) sum += R - L, L++; else R--; } return sum; } void solve(int u, int size) { centroid = 0; getCentroid(u, -1, size); getCentroid(centroid, -1, size); vis[centroid] = true; ans += calc(centroid, 0); for (auto i : g[centroid]) { if (vis[i.first]) continue; ans -= calc(i.first, i.second); // 容斥。去除错误的答案。 } for (auto i : g[centroid]) { if (vis[i.first]) continue; solve(i.first, siz[i.first]); } } 暂且咕咕咕。🕊 Luogu P4149 [IOI2011]Race:给定一棵有 $n$ 个点的带权树,给出 $k$,求一条简单路径。权值和等于 $k$,且边的数量最小。 $n\le 200000,k,w_i\le 1000000$ 开个桶数组记录最小边数即可。 RECORD。 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 // 桶数组,minn[i] 表示权值和为 i 的路径的最小边数 int minn[MAXK]; // d 表示权值和,b 表示边数 void getDis(int u, int fa, int d, int b) { if (d > k) return; dis.emplace_back(d, b); for (auto i : g[u]) { if (i.first == fa || vis[i.first]) continue; getDis(i.first, u, d + i.second, b + 1); } } void calc(int u) { minn[0] = 0; tdis.clear(); for (auto i : g[u]) { if (vis[i.first]) continue; dis.clear(); getDis(i.first, u, i.second, 1); for (auto j : dis) { if (minn[k - j.first] != -1) { // 可以拼凑成权值和为 k 的路径 if (ans == -1) ans = minn[k - j.first] + j.second; else ans = std::min(ans, minn[k - j.first] + j.second); } } // 更新桶数组 for (auto j : dis) { tdis.push_back(j); if (minn[j.first] == -1) minn[j.first] = j.second; else minn[j.first] = std::min(minn[j.first], j.second); } } // 还原 minn 数组 for (auto i : tdis) { minn[i.first] = -1; } } void solve(int u, int size) { centroid = 0; getCentroid(u, -1, size); getCentroid(centroid, -1, size); vis[centroid] = true; calc(centroid); for (auto i : g[centroid]) { if (vis[i.first]) continue; solve(i.first, siz[i.first]); } } Luogu P2634 [国家集训队]聪聪可可 Luogu P3714 [BJOI2017]树的难题 《挑战程序设计竞赛》 OI Wiki 🙇‍

2022/8/2
articleCard.readMore

第一次做主题:hugo-klay

上一次折腾博客已经是不知道什么时候的事情了。。 Hugo 这个框架很不错,不想更换,那就尝试做一个主题吧。 Hugo-klay 这个名字取自我最喜欢的 NBA 球员 Klay Thompson,不久前他随金州勇士队夺得了 2021-2022 赛季的 NBA 总冠军。 实话说,我的前端是零基础水平,JS 完全不会写。感谢非常容易上手的 tailwindcss 框架,同时还借(抄)鉴(袭)了很多 hugo-tania、hugo-theme-stack 和 hugo-tailwindcss-starter-theme 的代码。 就像前面说的那样,这个主题的代码质量比较差,而且主要也是自己使用,所以暂时不会单独为这个主题开一个 repo。如果你想参考,也可以到 ChungZH.github.io 这里寻找源码。有时间 的话,我会整理一下代码,再考虑单独分成一个项目。 自我感觉还是挺漂亮的,哈哈哈。

2022/7/25
articleCard.readMore

关于 int 与 long long 的运算速度

前言 写一道 CF 题的时候,算法明明是正确的,却一直都 TLE。最后把一个 long long 类型的数组改成了 int,竟然就 AC 了。。 这不禁引发了我的思考,int 与 long long 的运算速度不一样吗? 由于本菜鸡并没有什么计算机基础原理的知识,只好做了一个测试。当然,这个测试其实很不严谨,没有很大的参考价值。我也就图一乐,哈哈哈哈哈 电脑:Lenovo Yoga 14sACH 2021 系统:Windows 11 25163.1010 CPU:AMD Ryzen 7 5800H with Radeon Graphics (16) @ 3.200GHz RAM:16.0 GB 编译器:GCC 11.2.0 仅仅是为了图一乐, 我第一次使用了 Google Benchmark 这一工具。其实挺好上手的。 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 <benchmark/benchmark.h> using namespace benchmark; static void int_add(State &state) {   int a = std::rand(), b = std::rand(), c = 0;   for (auto _ : state)     DoNotOptimize(c = (++a) + (++b)); } static void ll_add(State &state) {   long long a = std::rand(), b = std::rand(), c = 0;   for (auto _ : state)     DoNotOptimize(c = (++a) + (++b)); } static void int_div(State &state) {   int a = std::rand(), b = std::rand(), c = 0;   for (auto _ : state)     DoNotOptimize(c = (++a) / (++b)); } static void ll_div(State &state) {   long long a = std::rand(), b = std::rand(), c = 0;   for (auto _ : state)     DoNotOptimize(c = (++a) / (++b)); } static void int_mod(State &state) {   int a = std::rand(), b = std::rand(), c = 0;   for (auto _ : state)     DoNotOptimize(c = (++a) % (++b)); } static void ll_mod(State &state) {   long long a = std::rand(), b = std::rand(), c = 0;   for (auto _ : state)     DoNotOptimize(c = (++a) % (++b)); } BENCHMARK(int_add)->Threads(8)->Iterations(1e9); BENCHMARK(ll_add)->Threads(8)->Iterations(1e9); BENCHMARK(int_div)->Threads(8)->Iterations(1e9); BENCHMARK(ll_div)->Threads(8)->Iterations(1e9); BENCHMARK(int_mod)->Threads(8)->Iterations(1e9); BENCHMARK(ll_mod)->Threads(8)->Iterations(1e9); BENCHMARK_MAIN(); 1 2 3 4 5 6 7 8 9 10 --------------------------------------------------------------------- Benchmark                                        Time             CPU --------------------------------------------------------------------- int_add/iterations:1000000000/threads:8      0.209 ns         1.57 ns ll_add/iterations:1000000000/threads:8       0.225 ns         1.71 ns int_div/iterations:1000000000/threads:8      0.302 ns         2.29 ns ll_div/iterations:1000000000/threads:8       0.306 ns         2.38 ns int_mod/iterations:1000000000/threads:8     0.345 ns 2.18 ns ll_mod/iterations:1000000000/threads:8       0.350 ns 2.34 ns 经过多次测试,long long 类型的各种运算都比 int 慢一点。 比较专业的一个解答。详见 performance - C++ int vs long long in 64 bit machine - Stack Overflow。 这里引用相关问答: 1) If it is best practice to use long long in x64 for achieving maximum performance even for for 1-4 byte data? No- and it will probably in fact make your performance worse. For example, if you use 64-bit integers where you could have gotten away with 32-bit integers then you have just doubled the amount of data that must be sent between the processor and memory and the memory is orders of magnitude slower. All of your caches and memory buses will crap out twice as fast. 可以不用 long long 就尽量不用。最好不要使用 #define int long long 这种粗暴手段。 致谢: C++ Benchmarking Tips for Beginners - Unum Blog 碎碎念:真的有大半年没写过博客了,上次更新还是寒假呢哈哈哈。一整个学期都好忙啊,接下来就是初三了呢。 🙇

2022/7/22
articleCard.readMore

Treap 笔记

Treap = Tree + Heap 在学习 Treap 之前,需要先了解一下二叉搜索树(BST, Binary Search Tree): 设 $x$ 是二叉搜索树中的一个结点。如果 $y$ 是 $x$ 左子树中的一个结点,那么 $y.key \lt x.key$。如果 $y$ 是 $x$ 右子树中的一个结点,那么 $y.key \gt x.key$。 BST 上的基本操作所花费的时间与这棵树的高度成正比。对于一个有 $n$ 个结点的二叉搜索树中,这些操作的最优时间复杂度为 $O(\log n)$,最坏为 $O(n)$。随机构造这样一棵二叉搜索树的期望高度为 $O(\log n)$。然而,当这棵树退化成链时,则同样的操作就要花费 $O(n)$ 的最坏运行时间。 由于普通 BST 容易退化,对于它的实现就不再赘述。在实践中需要使用如 Treap 这样的平衡二叉搜索树。 顾名思义,Treap 是树和堆的结合。它的数据结构既是一个二叉搜索树,又是一个二叉堆。 在 Treap 的每个结点中,除了 $key$ 值,还要保存一个 $fix$(更常见的是 $priority$)值。这个值是随机值,以它为依据来同时建立最大堆(或最小堆)。因为 $fix$ 值是随机的,所以可以让这棵树更加平衡,高度更接近 $O(\log n)$。它的各种操作期望时间复杂度都是 $O(\log n)$。 旋转式 Treap 的常数较小。 左旋/右旋操作不会破坏 BST 的性质,并可以通过它来维护堆,使树平衡。 如图,以右旋为例。假设现在左边树 $b$ 的 $fix$ 值大于 $a$ 的 $fix$ 值,然而 $b$ 是 $a$ 的儿子,那么就不符合最大堆的性质,需要进行右旋,变成了右边的树。 但是为什么在旋转的过程中没有破坏 BST 的性质呢?设 $c \in C, d \in D, e \in E$。由左树知 $c \lt b \lt d \lt a \lt e$。再由旋转之后树的结构可以得出 $c \lt b \lt d \lt a \lt e$,这两个式子是一样的。所以,这棵树依然是 BST。 在插入和删除操作中都要按需进行旋转操作。 根据 BST 的性质,找到相应的位置创建新叶子结点就可以了。如果不符合最大堆性质,进行旋转操作。 删除一个元素时,可以对被删除的结点分类讨论: 没有子结点:直接就成空的了 只有一个子结点:把被删除结点设成它仅有的儿子即可 有两个子结点:选出两个儿子中 $fix$ 值较大的一个,通过旋转操作把它设成新的根,这样要删除的结点就只有一个儿子了,按照情况 2 处理。这种方法保证满足了 BST 和最大堆的性质。 在程序实现时,实际上情况 1 和 2 的代码是一样的,所以只用分两类。 以 P3369 【模板】普通平衡树 - 洛谷 为例,代码如下。 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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 #include <algorithm> #include <cstdio> #include <cstdlib> #include <cstring> #include <iostream> #include <vector> using namespace std; const int INF = 1000000009; struct NODE { int val, fix, size; // size 指树的总结点数 NODE *left, *right; NODE(const int val) : val(val) { fix = rand(); left = right = NULL; size = 1; } }; void maintain(NODE *&p) { p->size = 1; if (p->left != NULL) p->size += p->left->size; if (p->right != NULL) p->size += p->right->size; } void rightRotate(NODE *&p) { NODE *tmp = p->left; p->left = tmp->right; tmp->right = p; p = tmp; maintain(tmp->right); maintain(tmp); } void leftRotate(NODE *&p) { NODE *tmp = p->right; p->right = tmp->left; tmp->left = p; p = tmp; maintain(tmp->left); maintain(tmp); } void insert(NODE *&p, const int value) { if (p == NULL) { p = new NODE(value); } else if (value <= p->val) { insert(p->left, value); if (p->left->fix < p->fix) rightRotate(p); } else { insert(p->right, value); if (p->right->fix < p->fix) leftRotate(p); } maintain(p); } int count(const NODE *p, const int value) { if (!p) return 0; if (p->val == value) return 1; if (value <= p->val) return count(p->left, value); return count(p->right, value); } void remove(NODE *&p, const int value) { if (!p) return; if (p->val == value) { if (p->left == NULL || p->right == NULL) { NODE *tmp = p; if (p->right) p = p->right; else p = p->left; delete tmp; } else if (p->left->fix < p->right->fix) { rightRotate(p); remove(p->right, value); maintain(p); } else { leftRotate(p); remove(p->left, value); maintain(p); } } else if (value < p->val) { remove(p->left, value); maintain(p); } else { remove(p->right, value); maintain(p); } } int getrank(const NODE *p, int value) { if (!p) return INF; int leftsize = 0; if (p->left != NULL) leftsize = p->left->size; if (p->val == value) return min(leftsize + 1, getrank(p->left, value)); else if (value < p->val) return getrank(p->left, value); else if (value > p->val) return leftsize + 1 + getrank(p->right, value); } int find(const NODE *p, int rank) { if (!p) return 0; int leftsize = 0; if (p->left != NULL) leftsize = p->left->size; if (leftsize >= rank) return find(p->left, rank); else if (leftsize + 1 == rank) return p->val; else return find(p->right, rank - leftsize - 1); } int getpre(const NODE *p, int value) { // 前驱 if (!p) return -INF; if (p->val >= value) return getpre(p->left, value); else return max(p->val, getpre(p->right, value)); } int getnext(const NODE *p, int value) { // 后继 if (!p) return INF; if (p->val <= value) return getnext(p->right, value); else return min(p->val, getnext(p->left, value)); } int n; NODE *root; int main() { scanf("%d", &n); while (n-- > 0) { int opt, x; scanf("%d %d", &opt, &x); if (opt == 1) { insert(root, x); } else if (opt == 2) { remove(root, x); } else if (opt == 3) { printf("%d\n", getrank(root, x)); } else if (opt == 4) { printf("%d\n", find(root, x)); } else if (opt == 5) { printf("%d\n", getpre(root, x)); } else if (opt == 6) { printf("%d\n", getnext(root, x)); } } return 0; } 无旋 Treap 的核心操作是分裂、合并。 分裂操作会将原 Treap 一分为二,第一个 Treap 中的结点关键值都小于等于 $key$,第二个中都大于 $key$。使用递归实现。 若当前关键值大于 $key$,那么当前结点连同右子树都属于第二个 Treap,继续往左子树递归。 若当前关键值小于等于 $key$,那当前结点连同左子树都属于第一个 Treap。继续往右子树递归。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pair<NODE *, NODE *> split(NODE *u, const int value) { if (u == nullptr) return make_pair(nullptr, nullptr); if (u->value > value) { auto tmp = split(u->ch[0], value); u->ch[0] = tmp.second; maintain(u); return make_pair(tmp.first, u); } else { auto tmp = split(u->ch[1], value); u->ch[1] = tmp.first; maintain(u); return make_pair(u, tmp.second); } } 合并函数接受两棵树,其中第一棵的值都小于第二棵。每一次根据两棵树的根的 $fix$ 值来确定新树的根,然后递归合并子树。 当两棵树任何一个为空时,返回另一个。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 NODE *merge(NODE *l, NODE *r) { if (l == nullptr) return r; if (r == nullptr) return l; if (l->fix < r->fix) { l->ch[1] = merge(l->ch[1], r); maintain(l); return l; } // else r->ch[0] = merge(l, r->ch[0]); maintain(r); return r; } 将树分裂成两个部分:$A=\lbrace x \mid x \le key \rbrace$、$B= \lbrace x \mid x \gt key \rbrace$,先将要插入的值合并入 $A$,最后合并 $A$ 和 $B$。 1 2 3 4 5 void insert(int value) { auto tmp = split(root, value); tmp.first = merge(tmp.first, new NODE(value)); root = merge(tmp.first, tmp.second); } 将树分裂成三个部分:$A= \lbrace x \mid x \lt key \rbrace$、$B= \lbrace x \mid x = key \rbrace$、$C= \lbrace x \mid x \gt key \rbrace$,然后合并 $A$ 和 $C$。 1 2 3 4 5 6 void erase(int value) { auto tmp = split(root, value - 1); auto tmp2 = split(tmp.second, value); delete tmp2.first; root = merge(tmp.first, tmp2.second); } 致谢: YWQ (Monad) OI Wiki CC BY-SA 4.0 《算法导论》 Algorithms for Competitive Programming CC BY-SA 4.0 Treap - Wikipedia CC BY-SA 3.0

2022/2/9
articleCard.readMore

告别 2021

这一年还是收获满满的。 上半年去了 GDOI 观摩神仙打架,长了见识。 CSP-J 2021 1=,挺满意的。 年末镇赛第一,虽然说没什么用但是就是挺开心的 😁 总的来说,进步挺大的。 是真正的初中生了。 几次大考,有成功,也有失利。可以说对于学习的心态变成熟了。 值得一提的是,十月份的时候换了一个校长,带来了很多新气象,幸福感不断提升。期待他在以后的表现。 爱上了 🏓 乒乓球,买了一个五百多的拍子,奢侈了一把,真爽( 对运动的热情高涨了很多。 总之,希望在新的一年,一切都会更好!

2022/1/1
articleCard.readMore

CSP-J 2021 游记

CSP-J/S 认证注意事项: …… 11. 祝各位选手好运。 初赛前有点小紧张。 赛前勉强做了几套试卷,然后就上考场了。 刚考完对答案的时候发现 J 组才 72,看洛谷上大家都说今年 J 组简单了 blabla,分数线肯定会升,然而我却觉得好难,那一个星期都害怕极了。。。结果分数出来了才发现洛谷那群人真是扯啊哈哈哈 S 组才 48 分,没有成功压线。(其实就算去了复赛也拿不了分。。 比赛前一晚上八点在学校出发。逃掉了晚自习(尽管是星期五 去到酒店大概也九点半了,洗完澡,看了会儿凤凰台,然后就睡了。 第二天早上六点半起床,吃完自助餐(和上一年的变化不大,挺好吃的),七点二十出发。 然后进考场。 电脑好像是 Ryzen 3600,8GB 内存。 八点半开考。 密码很乱,6ewid\n16384#,监考员一开始还直接忽略最后面那个井号了。。。 打开题目,发现第一第二题题面好长,有点慌了起来… T2 尤为毒瘤,到了 9:22 才搞完了。。。感觉挺很危险的,好怕翻车。 10:08,肝完 T3,77 行代码,写完人都瘫了。。感谢第三个样例,一个一个找情况。。。 (不知道怎么比对两个文件的内容,于是直接打开 Sublime Text 开始用查找来找不同。。。 赶紧吃了根士力架,然后去上了个厕所。 10:55,T4 过样例了,打得比 T3 轻松多了,当然也不可能拿满分。。。其实也不知道该怎么做,直接乱搞,做法非常诡异。。看看效果怎样吧,能骗到 50 分就是胜利。 还有一点,今年 NOI Linux 2 还行。(毕竟上一年给了虚拟机但是系统有密码,根本打不开,笑死)在里面编译了几次代码,虽然也没什么用。运行起来还蛮快的。 总的来说,这次考得还行。题目有点诡异,没有 dp,没有搜索,个人感觉侧重考基本功。 等成绩吧。 UPD 1 (2021/10/23 22:50): 广东源代码出了。 candy: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <bits/stdc++.h> using namespace std; long long n, L, R; int main() { freopen("candy.in", "r", stdin); freopen("candy.out", "w", stdout); cin >> n >> L >> R; long long LRCHA = (R-L+1); long long LCN = L/n, RCN = R/n; if (RCN > LCN) { cout << n-1 << endl; return 0; } cout << R-LCN*n << endl; return 0; } 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 #include <bits/stdc++.h> using namespace std; int n, Q; long long a[8005]; int idxa[8005]; bool cmp(int i1, int i2) { if (a[i1] != a[i2]) return a[i1] < a[i2]; return i1 < i2; } int main() { freopen("sort.in", "r", stdin); freopen("sort.out", "w", stdout); scanf("%d%d", &n, &Q); for (int i = 1; i <= n; i++) { scanf("%lld", &a[i]); } for (int i = 1; i <= n; i++) idxa[i] = i; sort(idxa+1, idxa+1+n, cmp); while (Q--) { int type; scanf("%d", &type); if (type == 1) { // �޸� int x; long long v; scanf("%d%lld", &x, &v); a[x] = v; for (int i = 1; i <= n; i++) idxa[i] = i; sort(idxa+1, idxa+1+n, cmp); } else if (type == 2) { // ��ѯ int x; scanf("%d", &x); for (int i = 1; i <= n; i++) { if (idxa[i] == x) { printf("%d\n", i); break; } } } } return 0; } network: 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 #include <bits/stdc++.h> using namespace std; int n; bool checkPrevZero(const string number) { if (number.length() > 1 && number[0] == '0') return true; return false; } int strToInt(const string number) { int t = 0; for (int i = 0; i < number.length(); i++) { t *= 10; t += number[i]-'0'; } return t; } bool check(const string addr) { int cntDot = 0, cntMao = 0; int dotIdx[3], maoIdx; if (!(addr[0] >= '0' && addr[0] <= '9') || !(addr[addr.length()-1] >= '0' && addr[addr.length()-1] <= '9')) return false; for (int i = 0; i < addr.length(); i++) { if (addr[i] == '.') { if (cntDot > 2) return false; if (!(addr[i-1] >= '0' && addr[i-1] <= '9')) return false; cntDot++; dotIdx[cntDot-1] = i; } else if (addr[i] == ':') { if (cntMao > 1) return false; if (!(addr[i-1] >= '0' && addr[i-1] <= '9')) return false; cntMao++; maoIdx = i; } else if (!(addr[i] >= '0' && addr[i] <= '9')) { return false; } } if (!(cntDot == 3 && cntMao == 1)) return false; if (maoIdx < dotIdx[2]) return false; string n1 = addr.substr(0, dotIdx[0]), n2 = addr.substr(dotIdx[0]+1, dotIdx[1]-dotIdx[0]-1), n3 = addr.substr(dotIdx[1]+1, dotIdx[2]-dotIdx[1]-1), n4 = addr.substr(dotIdx[2]+1, maoIdx-dotIdx[2]-1), n5 = addr.substr(maoIdx+1, addr.length()-maoIdx-1); //cout << n1 << " " << n2 << " " << n3 << " " << n4 << " " << n5 << endl; if (checkPrevZero(n1) || checkPrevZero(n2) || checkPrevZero(n3) || checkPrevZero(n4) || checkPrevZero(n5)) return false; if (n1.length() > 3 || n2.length() > 3 || n3.length() > 3 || n4.length() > 3 || n5.length() > 5) return false; int a = strToInt(n1), b = strToInt(n2), c = strToInt(n3), d = strToInt(n4), e = strToInt(n5); if (a > 255 || b > 255 || c > 255 || d > 255 || e > 65535) return false; return true; } int serversNum = 0; map<string, int> servers; int main() { freopen("network.in", "r", stdin); freopen("network.out", "w", stdout); cin >> n; string op, ad; for (int i = 0; i < n; i++) { cin >> op >> ad; if (!check(ad)) { cout << "ERR\n"; continue; } if (op == "Server") { if (servers[ad] != 0) { cout << "FAIL\n"; continue; } servers[ad] = i+1; cout << "OK\n"; } else if (op == "Client") { if (servers[ad] == 0) { cout << "FAIL\n"; continue; } cout << servers[ad] << endl; } } return 0; } fruit: (代码非常奇妙,思路十分无理,建议不要看) 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 #include <bits/stdc++.h> using namespace std; struct node { int idx; int val; int ai; int len; }; int n; int a[200005]; vector<node> sons[405005]; int nodeNums = 1; void buildtree(int root, int left, int right, bool rrr) { if (rrr) { int prev = 1; for (int i = 2; i <= n; i++) { if (a[i] != a[prev]) { sons[1].push_back({nodeNums+1, a[prev], prev, i-prev}); buildtree(nodeNums+1, prev, i-1, 0); nodeNums++; prev = i; } } sons[1].push_back({nodeNums+1, a[prev], prev, n-prev+1}); buildtree(nodeNums+1, prev, n, 0); nodeNums++; } else { for (int i = left; i <= right; i++) { sons[root].push_back({++nodeNums, a[left], i, 1}); } } } void work() { for (int i = 0; i < sons[1].size(); i++) { printf("%d ", sons[sons[1][i].idx][0].ai); sons[sons[1][i].idx].erase(sons[sons[1][i].idx].begin()); if (sons[sons[1][i].idx].size() == 0) { sons[1].erase(sons[1].begin()+i); i--; } } // merge for (int i = 1; i < sons[1].size(); i++) { if (sons[1][i].val != sons[1][i-1].val) continue; int id = sons[1][i].idx; for (int j = 0; j < sons[id].size(); j++) { sons[sons[1][i-1].idx].push_back(sons[id][j]); } sons[1].erase(sons[1].begin()+i); i--; } printf("\n"); } int main() { freopen("fruit.in", "r", stdin); freopen("fruit.out", "w", stdout); scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); } buildtree(1, 1, n, 1); while (sons[1].size()) { work(); } return 0; } 洛谷自测:342 = 100 + 72 + 100 + 70 计蒜客自测:342 = 100 + 52 + 100 + 90 目前看上去 1= 稳了。 UPD 2 (2021/10/30 22:38): CCF 官方:100 + 52 + 100 + 70 = 322

2021/10/24
articleCard.readMore

线段树笔记

线段树是一种高端的数据结构,可以用来在区间上进行信息统计。它能够在 $O(logN)$ 的时间复杂度内实现单点/区间修改、区间找最大值/最小值/总和/…,适用于大规模的区间统计。 如下图就是一棵线段树。在结点中,你可以存对应区间的最大值,最小值,总和等等。 对于每一个结点 $i$,它的两个子结点分别是 $2i$ 和 $2i+1$。因此,在开树的数组时,最好要开到 $4N$ 的大小。 关于 $4N$,详见 OI-Wiki。 下面是一个求区间和的线段树的建树代码。 通过 DFS 建树,到叶结点,然后一路回溯求出和。 1 2 3 4 5 6 7 8 9 10 11 12 13 void build_tree(int cur, int left, int right) { // cur 为当前树的根,[left, right] 是当前树对应的区间 if (left == right) { // 到叶子节点了,区间长度为 1,总和就是它本身 tree[cur] = a[left]; return ; } int leftSon = cur*2, rightSon = leftSon+1; int mid = (left+right)/2; build_tree(leftSon, left, mid); build_tree(rightSon, mid+1, right); tree[cur] = tree[leftSon]+tree[rightSon]; // 求和 } 这里求 leftSon,rightSon,mid 的模式在线段树的所有操作中都会用到。 对于一个不恰好的区间,我们可以不断地把它拆分成两个恰好的区间再进行合并。 如图所示,拆分过程如下: [3, 7] [3, 4], [5, 7] [3, 4], [5, 6], [7, 7] 在实际 DFS 过程中,我们可以分为三种情况: 当前结点对应区间和要查询的区间完全无关,直接退出 当前结点对应区间完全处于要查询的区间范围,返回当前结点的值 两个区间部分相交,继续拆分为 1 或 2 情况 1 2 3 4 5 6 7 8 9 long long query(int cur, int l, int r, int x, int y) { // cur 为当前树的根,[l, r] 是当前树对应的区间,[x, y] 是要查询的区间 if (y < l || r < x) return 0; // 1. 相离 if (x <= l && r <= y) return tree[cur]; // 2. 完全包含 // 3. 相交 int leftSon = cur*2, rightSon = leftSon+1; int mid = (l+r)/2; return query(leftSon, l, mid, x, y) + query(rightSon, mid+1, r, x, y); // 求和 } 单点修改并没有什么意思,就不讲了。 区间修改当然不是重复做单点修改,否则使用线段树就很没有必要了。为了避免走到底下去,我们要使用一个懒惰标记(lazy tag)。当一个大区间内所有的小单位都要进行同样的修改操作时,只需要在大区间做一次标记就可以了。到了必须要走下去(即查询更小的区间或修改更小的区间)的时候,再把懒惰标记下放。 下面以一个区间增加一个值并进行区间查询为例。 首先定义一个结构体,其中 add 就是记录这个区间需要增加的值。 1 2 3 4 struct node { long long sum, add; } tree[4 * MAXN]; 下放操作: 1 2 3 4 5 6 7 8 9 10 11 12 13 void pushdown(int cur, int left, int mid, int right) { // 将当前结点的 add 值下放给子结点 const int leftSon = cur * 2, rightSon = leftSon + 1; // 更新总和(区间元素个数*每个元素要增加的值) tree[leftSon].sum += (mid - left + 1) * tree[cur].add; tree[rightSon].sum += (right - mid) * tree[cur].add; // 更新 add 值 tree[leftSon].add += tree[cur].add; tree[rightSon].add += tree[cur].add; // 当前结点的懒惰标记已经下放,需要清零 tree[cur].add = 0; } 区间增加一个值: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void update(int cur, int left, int right, int x, int y, ll c) { if (left > y || right < x) // 相离 return; if (left >= x && right <= y) // 完全包含 { // 给当前区间整体增加 c tree[cur].sum += (right - left + 1) * c; // 打标记 tree[cur].add += c; // 这时不需要子结点的值,所以不必下放 return; } // 相交 // 分成两个子区间,必须要往下走了 int mid = (left + right) / 2; int leftSon = cur * 2, rightSon = leftSon + 1; pushdown(cur, left, mid, right); // 先下放标记 update(leftSon, left, mid, x, y, c); update(rightSon, mid + 1, right, x, y, c); tree[cur].sum = tree[2 * cur].sum + tree[2 * cur + 1].sum; // 最后汇总总和 } 区间查询: 1 2 3 4 5 6 7 8 9 10 11 12 13 ll query(int cur, int left, int right, int x, int y) { if (left > y || right < x) // 相离 return 0; if (left >= x && right <= y) // 完全包含 return tree[cur].sum; // 相交 // 拆成两个字区间再求和 int mid = (left + right) / 2; int leftSon = cur * 2, rightSon = leftSon + 1; pushdown(cur, left, mid, right); // 必须往下走,下放标记 return query(leftSon, left, mid, x, y) + query(rightSon, mid + 1, right, x, y); } 通过上面的代码,我们可以看到:懒惰标记就是为了不往下走,尽量在更大的区间做一次操作。必要时才往下下放标记。它不会主动做事,而是到你需要的时候才花时间去做。通过这样节省了很多时间。 总的来说,线段树不是一个很难的数据结构,但是很实用。 感谢 lgj 老师!

2021/8/4
articleCard.readMore

Hello Hugo!

暑假来了,顺便把博客更新一下。 从以前的 Vuepress 变成了 Hugo,速度真的快了很多,不愧是 “The world’s fastest”。用的主题是 Tania,很简洁、漂亮。Hugo 非常易用,不到半天就完整迁移过来了。我可以很肯定地说这一次博客迁移是有史以来最快的一次。 CI 用的是 GitHub Actions: 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 name: github pages on: push: branches: - hugo # Set a branch to deploy pull_request: jobs: deploy: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 with: submodules: true # Fetch Hugo themes (true OR recursive) fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: 'latest' # extended: true - name: Build run: hugo --minify - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_branch: master # default: gh-pages 总之就是非常丝滑,Hugo NB!

2021/7/16
articleCard.readMore

CSP-J 2020 游记

他山之石,可以攻玉。 入学你校几乎两个月都在搞初赛。 结果初一还是只有两个人过了 最后是 73.5 分,水过去了。 Day 0 要去大学城的广大附中,就去住酒店了。 在 tjl 大佬房间里 玩,其实是在看凤凰台。 依稀记得那晚上拜登和特朗普的比分是 264 : 214,林郑去北京见韩正了。 CCTV-7 上面中科院在帮农民种橘子? 十点多就回去昏昏沉沉地睡了,还挺香( 早上六点半就醒了。 在酒店吃了顿自助早餐,真香。 到处都是石实的大佬 %%% 。 然后搭着同校热心家长的车前往广大附中。 门口还挺热闹的,好像发生了许多事情: 黄老师身份证不见了,其实藏在袋子里 某大佬没带准考证 还有没带粤康码的 于是感到很庆幸,没入考场的时候也是一场考验。。所以说带齐资料很重要。 很快就进考场了。 电脑有 8G 内存,装的是 Windows 10 神州网信政府版,感觉只是开始菜单看上去稍有不同。 下发题目之后几分钟我还在打 A+B,打完之后才一愣一愣地抄密码解压题目。 翻了一下,发现第一题好难,于是从第二题开始做。 T2 刚开始竟然用了 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 #include <iostream> #include <cstdio> #include <fstream> #include <algorithm> using namespace std; bool cmp(int a, int b) { return a > b; } int a[100005]; int main() { int n, w; scanf("%d%d", &n, &w); for (int i = 0; i < n; i++) { int inp; scanf("%d", &inp); int j = 0; for (; j < i; j++) { if (a[j] < inp) break; } for (int k = i; k >= j; k--) { a[k+1] = a[k]; } a[j] = inp; int planNum = max(1, (int)((i+1)*0.01*w)); printf("%d ", a[planNum-1]); } return 0; } 然后 T3 看了好久才弄懂,就暴力 stack 了。 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 #include <iostream> #include <stack> #include <algorithm> #include <string> #include <fstream> #include <cstdio> using namespace std; int n; int a[100003]; int main() { string expr; getline(cin, expr); const int exprlen = expr.length(); scanf("%d", &n); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); int q; scanf("%d", &q); stack<int> s; for (int i = 0; i < q; i++) { int t; scanf("%d", &t); a[t] = !a[t]; string cur; for (int j = 0; j < exprlen+1; j++) { if (j == exprlen || expr[j] == ' ') { if (cur == "!") { int t = s.top(); s.pop(); s.push(!t); } else if (cur == "&") { int t = s.top(); s.pop(); int t2 = s.top(); s.pop(); s.push(t && t2); } else if (cur == "|") { int t = s.top(); s.pop(); int t2 = s.top(); s.pop(); s.push(t || t2); } else if (cur[0] == 'x') { int xb = 0; for (int k = 1; k < cur.length(); k++) { xb *= 10; xb += cur[k]-'0'; } s.push(a[xb]); } cur.clear(); } else cur += expr[j]; } printf("%d\n", s.top()); if (!s.empty()) s.pop(); a[t] = !a[t]; } return 0; } 接下来才开始构思第一题,一直摸不着头绪,就随随便便写了一个爆搜,枚举 2 的幂相加。 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 <iostream> #include <cstdio> #include <fstream> #include <algorithm> using namespace std; int n; const int a[23] = {2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608}; int ansn = 0, ans[24]; int dfs(int step, int sum) { if (sum > n) return 0; if (step > 23) return 0; if (sum == n) { return 1; } for (int i = 1; i <= 22; i++) { int t1 = dfs(step+i, sum + a[step]); if (t1) { ans[ansn++] = a[step]; return 1; } } } int main() { cin >> n; if (n % 2 != 0) { cout << -1 << endl; return 0; } for (int i = 0; i < 23; i++) { if (n == a[i]) { cout << a[i] << endl; return 0; } } dfs(0, 0); for (int i = 0; i < ansn; i++) { cout << ans[i] << ' '; } return 0; } 第四题直接无脑爆搜,能拿到暴力分就 ok。 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 #include <iostream> #include <cstdio> #include <fstream> #include <algorithm> using namespace std; long long ans = -99999999999; int a[1005][1005], n, m; bool vis[1005][1005]; const int dx[3] = { -1, 1, 0 }; const int dy[3] = { 0, 0, 1 }; void dfs(int x, int y, long long sum) { if (x == n && y == m) { // 终点 ans = max(ans, sum+a[x][y]); return ; } for (int i = 0; i < 3; i++) { int nx = x + dx[i], ny = y + dy[i]; if (nx < 1 || ny < 1 || nx > n || ny > m) continue; if (vis[nx][ny]) continue ; vis[nx][ny] = 1; dfs(nx, ny, sum + a[x][y]); vis[nx][ny] = 0; } } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { scanf("%d", &a[i][j]); vis[i][j] = false; } } dfs(1, 1, 0); printf("%d\n", ans); return 0; } 出了考场之后和 tjl 对答案,才知道第一题考的是二进制,第二题要用桶排序。。。 然后那时竟然还挺乐观的,觉得我的也没问题。。。 甚至还开始讨论有没有 1= 。。。。。。。。。 直到洛谷上了民间数据自测。 100 + 80 + 30 + 15 = 225 还是看看能不能有二等吧。。 总之达到了考前的期望,考后还是太自信了一点点。也没什么遗憾,正常发挥出了自己的水平。 那么就: CSP-J/S 2021 RP++ UPD: 215 分,2=

2020/11/15
articleCard.readMore

Notepanda 开发小结

前言 最近开始学习 Qt,然后就挖坑了一个小玩具 Notepanda,顺便看看能不能替代诸如 notepad、gedit 之类的软件。顺便锻炼一下自己。 GitHub repo 基本的文本编辑操作(没想到吧这也是 feature 了哈哈哈哈哈哈) 行号显示 语法高亮 从命令行启动。(如 notepanda 或者 notepanda CMakeLists.txt) 除此之外还实现了自定义字体、字号、Qt 主题和 Status Bar 等一些小功能。 以后的目标是实现多标签页,并对 Markdown 做一点优化(比如预览),如果有可能还会加进去一个 terminal。计划在 GitHub Projects。 目前还是很弱的一个东西,不过等查找、替换等 feature 实现之后,基本上可以替代 Windows 的 notepad 了。 这部分大概讲讲思路吧。 使用了 Qt 提供的 QPlainTextEdit 类,适合纯文本编辑。刚开始还用的是 QTextEdit,想想自己真是傻了,又不是要编辑富文本哈哈哈。 不过 QPlainTextEdit 似乎有点点慢,我也没能力造轮子,于是就将就着用吧。 刚开始想实现的时候看见了 Qt 官方的 Syntax Highlighter Example,很棒对吧。但是我可不想花精力去写一堆语言的规则呢! 然后就找到了 KDE Framework 里的 KSyntaxHighlighting。KDE Framework 是真的烦人,刚开始怎么也 build 不出来。等到了 GitHub Actions 上,整整用了四天时间才搞定,还是在某 Packman 的帮助下才完成的。。。当时看到绿绿的 Actions,我差点没开心得疯掉。。 这个 KSyntaxHighlighting,deepin-editor 和 Qt Creator 都在使用。好在他自己也提供了一些 example,看上去很简单。依赖也很小,只有 Extra CMake Modules,不过看上去和高亮的功能没啥关系,也许是 KF 必备依赖吧。它自带了两百多种语言的高亮规则,省了我很多事情,有 Dark / Light 主题,不服还可以自己写。很满意。 我可不想每次更新都自己打一次包,没那个闲心,手上能用的系统也不够 :) 穷孩子怎么买得起 Mac。所以只能用 CI 啦。 现在 CI 主要帮我解决了: Windows 安装程序 Windows 上的 7z & MacOS 上的 dmg & Linux 的 AppImage Release 时自动上传以上所有文件 CI 平台当然是选择了 GitHub Actions 啦!现在这个项目所有环节都在 GitHub 上能找到,AUR 除外 :( Notepanda 的 CI 全都是抄 Qv2ray 上的,可真是帮了我很多忙。 感谢 Qv2ray 的 Super Packman: ymshenyu,感谢死鬼 gcc,感谢鸭鸭,感谢 Qv2ray User Group 里面的每一个人。如果没有他们,我的 Qt 旅程不会这么顺利。 顺便,Qv2ray 是一个很好用的 v2ray 跨平台客户端,欢迎尝试! 谢谢阅读 🙇‍♂️ 最后,放上我画的一只小熊猫:

2020/5/16
articleCard.readMore

Windows 中 C++ 测量时间的 N 种方法

在开发中经常需要测量时间,比如性能优化时,比较两种方法的耗时。除此之外,获取当前的时间也可以用于初始化随机数生成器。而 C++ 提供了很种方法来获取时间。 std::time 这个函数会返回一个距离 UTC 时间 1970 年 1 月 1 日 0:00 的秒数。 头文件:<ctime> 文档:std::time 1 std::time_t time( std::time_t* arg ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <windows.h> #include <ctime> #include <iostream> using namespace std; int main() { time_t res_1 = time(nullptr); Sleep(1000); // do some stuff... time_t res_2 = time(nullptr); cout << res_1 << endl; cout << asctime(localtime(&res_1)) << endl; cout << res_2 - res_1 << 's' << endl; return 0; } /* 输出结果 1585457981 Sun Mar 29 12:59:41 2020 1s */ 定义 返回表示当前时间的时间点。 这个时钟是专门用来计算时间的间隔的。C++ 还提供了一个 system_clock,用于获取系统的时间。 头文件:chrono 文档:std::chrono::stadey_clock 1 2 static std::chrono::time_point<std::chrono::steady_clock> now() noexcept; // (C++11 起) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <windows.h> #include <chrono> #include <iostream> using namespace std; int main() { chrono::steady_clock::time_point start = chrono::steady_clock::now(); Sleep(1000); // do some stuff... chrono::steady_clock::time_point end = chrono::steady_clock::now(); chrono::duration<double> ans = end - start; cout << ans.count() << 's' << endl; return 0; } /* 输出: 1.01495s */ GetTickCount 检测自系统启动以来经过的毫秒数,最多为 49.7 天。如果不够用,可以使用 GetTickCount64。 文档:GetTickCount function 1 DWORD GetTickCount(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <windows.h> #include <iostream> using namespace std; int main() { DWORD start = GetTickCount(); Sleep(1000); // do some stuff... DWORD end = GetTickCount(); cout << end - start << "ms" << endl; return 0; } /*输出: 1016ms */ 定义 检测自系统启动以来经过的毫秒数。 文档:GetTickCount64 function 1 ULONGLONG GetTickCount64(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <windows.h> #include <iomanip> #include <iostream> using namespace std; int main() { ULONGLONG start = GetTickCount64(); Sleep(1000); // do some stuff... ULONGLONG end = GetTickCount64(); cout << end - start << "ms" << endl; return 0; } /* 输出: 1015ms */ 定义 检测性能计数器的当前值,该值是一个高分辨率(<1us)时间戳,可用于时间间隔测量。 这个函数是一个「黑科技」,精度非常高,用起来也稍微麻烦一点点。 文档:QueryPerformanceCounter function 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 #include <windows.h> #include <iostream> using namespace std; int main() { LARGE_INTEGER freq; QueryPerformanceFrequency(&freq); // 获取性能计数器的频率 LARGE_INTEGER start; QueryPerformanceCounter(&start); Sleep(1000); // do some stuff... LARGE_INTEGER end; QueryPerformanceCounter(&end); double misTime = (end.QuadPart - start.QuadPart) / (freq.QuadPart / 1000000.0); // 微秒 double milTime = (end.QuadPart - start.QuadPart) / (freq.QuadPart / 1000.0); // 毫秒 double sTime = (end.QuadPart - start.QuadPart) / freq.QuadPart; // 秒 cout << misTime << " microseconds" << endl; cout << milTime << " milliseconds" << endl; cout << sTime << " seconds" << endl; return 0; } /* 输出: 1.00367e+06 microseconds 1003.67 milliseconds 1 seconds */ 至此,本文共介绍了五种测量时间段的方法。 作者很菜,如发现什么问题,请在评论区中指出。 谢谢 🙇‍♂️ Kurt Guntheroth. Optimized C++: Proven Techniques for Heightened Performance CppReference Microsoft Docs

2020/3/29
articleCard.readMore

优雅地使用 C++ 制作表格:tabulate

0x00 介绍 tabulate tabulate 是一个使用 C++ 17 编写的库,它可以制作表格。使用它,把表格对齐、格式化和着色,不在话下!你甚至可以使用 tabulate,将你的表格导出为 Markdown 代码。下图是一个使用 tabulate 制作的表格输出在命令行的样例: 当然,除了表格,你还可以玩出花样。看见下面这个马里奥了吗?这也是用 tabulate 制作的!源码在 这里。 首先你需要安装 CMake。 创建一个文件夹(下文用 X 代替),作为你使用 tabulate 的地方。再将 include 这个文件夹下载到 X 里。然后在 X 里创建 main.cpp 以及一个 CMakeLists.txt。 注意:需要下载 include 整个文件夹而不是仅仅下载 tabulate 文件夹 你可以点击 这里 下载 tabulate 项目,然后将 include 文件夹复制到 X 中。 将下面的代码复制进 CMakeLists.txt : 1 2 3 4 5 6 7 8 9 10 11 cmake_minimum_required(VERSION 3.8) # 这里的 tabulateDemo 可以换为你喜欢的名字 project(tabulateDemo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) include_directories(include) add_executable(main main.cpp) 最后 X 文件夹的结构应该是这样的: 1 2 3 4 5 . ├── CMakeLists.txt ├── include │ └── tabulate └── main.cpp 请认真核对好 X 的结构! 可前往 ChungZH/tabulatedemo 核对文件结构。 将下面这段代码复制进 main.cpp 中: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include "tabulate/table.hpp" using namespace std; using namespace tabulate; int main() { Table hellogithub; // 创建一个叫做 hellogithub 的 Table 对象 hellogithub.add_row({"HelloGitHub"}); hellogithub.add_row({"hellogithub.com"}); hellogithub[1][0].format() .font_style({fonttyle::underline}); hellogithub.add_row({"github.com/521xueweihan/HelloGitHub"}); hellogithub[2][0].format() .font_style({fonttyle::underline}); hellogithub.add_row({"xueweihan NB!!!"}); cout << hellogithub << endl; return 0; } 如果你使用的是 Linux/MacOS 系统,请在终端进入 X 文件夹并输入以下命令: 1 2 3 4 5 mkdir build cd build cmake .. make ./main 如果你使用的是 Windows 系统和 MinGW,请检查是否安装 mingw32-make.exe,并在终端中进入 X 文件夹,输入: 1 2 3 4 5 mkdir build cd build cmake .. mingw32-make ./main.exe 如果你使用 Windows 以及 MSVC,在终端中输入: 1 2 3 mkdir build cd build cmake .. 然后使用 Visual Studio 打开 build 文件夹下的 tabulateDemo.sln 来运行。 如果没有问题,那么你应该会在终端里看到: 请先认真分析 0x20 小试身手 章节中的代码并尝试着修改一下它! 为了防止表格中的内容过长导致不整齐,你可以指定表格每一列的宽度,tabulate 就会自动帮你换行。语法如下: 1 2 // 将表格第 0 行第 0 列的宽度设为20 table[0][0].format().width(20); 除了自动换行,你也可以在内容中使用 \n 来手动设置换行。 这是一个 Word Wrapping 的例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include "tabulate/table.hpp" using namespace std; using namespace tabulate; int main() { Table table; table.add_row({"This paragraph contains a veryveryveryveryveryverylong word. The long word will break and word wrap to the next line.", "This paragraph \nhas embedded '\\n' \ncharacters and\n will break\n exactly where\n you want it\n to\n break."}); table[0][0].format().width(20); // 设置第 0 行第 0 列的宽度为 20 table[0][1].format().width(50); // 设置第 0 行第 1 列的宽度为 50 cout << table << endl; return 0; } return 0; } 第 0 行第 0 列的文字是不是很长?但是设置了它的宽度后,就不用担心了。tabulate 将会帮你自动换行。如果不设置的话,表格就会变得很不整齐,你也可以尝试一下。 第 0 行第 1 列的内容里运用了\n 的换行符,所以即使我们给它设置了 50 的宽度,也会先根据内容里的 \n 换行符来换行。 值得注意的是,tabulate 会自动删除每一行内容两边的空白字符。 tabulate 支持三种对齐设置:左、中和右。默认情况下,全部内容都会靠左对齐。 要手动设置对齐方式,可以使用 .format().font_align(方向)。 举一个例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include "tabulate/table.hpp" using namespace std; using namespace tabulate; int main() { Table hellogithub; hellogithub.add_row({"HelloGitHub"}); hellogithub[0][0].format() .font_align(FontAlign::center); // 设置居中对齐 hellogithub.add_row({"hellogithub.com"}); hellogithub[1][0].format() .font_align(FontAlign::left); // 设置靠左对齐 hellogithub.add_row({"github.com/521xueweihan/HelloGitHub"}); hellogithub[2][0].format() .font_align(FontAlign::center); // 设置居中对齐 hellogithub.add_row({"xueweihan NB!!!"}); hellogithub[3][0].format() .font_align(FontAlign::right); // 设置靠右对齐 hellogithub[0][0].format().width(50); cout << hellogithub << endl; return 0; } tabulate 支持以下八种字体样式: 粗体 bold 深色 dark 斜体 italic 下划线 underline 闪烁 blink (?) 翻转 reverse 隐藏 concealed 删除线 crossed 某些样式可能会因为终端的原因而无法显示。 如:粗体、深色、斜体、闪烁等样式,请慎用。 要使用这些样式,可以调用 .format().font_style({...})。样式也可以叠加使用。 你可以对表格的字体、边框、角以及列分隔符号设置它们的前景或背景颜色。 tabulate 支持 8 种颜色: 灰色 gray 红色 red 绿色 green 黄色 yellow 蓝色 blue 洋红色 magenta 青色 cyan 白色 white 可以通过 .format().&lt;element&gt;_color(颜色) 的方式定义前景色或通过 .format().&lt;element&gt;_background_color(颜色) 的方式定义背景色。 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 #include "tabulate/table.hpp" using namespace tabulate; using namespace std; int main() { Table colors; colors.add_row({"Font Color is Red", "Font Color is Blue", "Font Color is Green"}); colors.add_row({"Everything is Red", "Everything is Blue", "Everything is Green"}); colors.add_row({"Font Background is Red", "Font Background is Blue", "Font Background is Green"}); colors[0][0].format() .font_color(Color::red) .font_style({fonttyle::bold}); colors[0][1].format() .font_color(Color::blue) .font_style({fonttyle::bold}); colors[0][2].format() .font_color(Color::green) .font_style({fonttyle::bold}); colors[1][0].format() .border_left_color(Color::red) .border_left_background_color(Color::red) .font_background_color(Color::red) .font_color(Color::red); colors[1][1].format() .border_left_color(Color::blue) .border_left_background_color(Color::blue) .font_background_color(Color::blue) .font_color(Color::blue); colors[1][2].format() .border_left_color(Color::green) .border_left_background_color(Color::green) .font_background_color(Color::green) .font_color(Color::green) .border_right_color(Color::green) .border_right_background_color(Color::green); colors[2][0].format() .font_background_color(Color::red) .font_style({fonttyle::bold}); colors[2][1].format() .font_background_color(Color::blue) .font_style({fonttyle::bold}); colors[2][2].format() .font_background_color(Color::green) .font_style({fonttyle::bold}); cout << colors << endl; return 0; } 你可以对表格的边框和角的文本、颜色或背景颜色进行自定义。 你可以使用 .corner(..)、.corner_color(..) 和 corner_background_color(..)  来对所有的角设置一个共同的样式。你也可以使用  .border(..) 、.border_color(..) 和  .border_background_color(..)  来对所有的边框设置一个共同的样式。 这是一个单独设定所有边框和角的示例: 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 <tabulate/table.hpp> using namespace tabulate; int main() { Table table; table.add_row({"ᛏᚺᛁᛊ ᛁᛊ ᚨ ᛊᛏᛟᚱy ᛟᚠᚨ ᛒᛖᚨᚱ ᚨᚾᛞ\n" "ᚨ ᚹᛟᛚᚠ, ᚹᚺᛟ ᚹᚨᚾᛞᛖᚱᛖᛞ ᛏᚺᛖ\n" "ᚱᛖᚨᛚᛗᛊ ᚾᛁᚾᛖ ᛏᛟ ᚠᚢᛚᚠᛁᛚᛚ ᚨ ᛈᚱᛟᛗᛁᛊᛖ\n" "ᛏᛟ ᛟᚾᛖ ᛒᛖᚠᛟᚱᛖ; ᛏᚺᛖy ᚹᚨᛚᚲ ᛏᚺᛖ\n" "ᛏᚹᛁᛚᛁᚷᚺᛏ ᛈᚨᛏᚺ, ᛞᛖᛊᛏᛁᚾᛖᛞ ᛏᛟ\n" "ᛞᛁᛊcᛟᚹᛖᚱ ᛏᚺᛖ ᛏᚱᚢᛏᚺ\nᛏᚺᚨᛏ ᛁᛊ ᛏᛟ cᛟᛗᛖ."}); table.format() .multi_byte_characters(true) // Font styling .font_style({fonttyle::bold, fonttyle::dark}) .font_align(FontAlign::center) .font_color(Color::red) .font_background_color(Color::yellow) // Corners .corner_top_left("ᛰ") .corner_top_right("ᛯ") .corner_bottom_left("ᛮ") .corner_bottom_right("ᛸ") .corner_top_left_color(Color::cyan) .corner_top_right_color(Color::yellow) .corner_bottom_left_color(Color::green) .corner_bottom_right_color(Color::red) // Borders .border_top("ᛜ") .border_bottom("ᛜ") .border_left("ᚿ") .border_right("ᛆ") .border_left_color(Color::yellow) .border_right_color(Color::green) .border_top_color(Color::cyan) .border_bottom_color(Color::red); std::cout << table << std::endl; return 0; } 一个一个设置表格的样式是不是很麻烦?tabulate 提供了迭代器,支持对表、行和列的迭代,更方便地格式化表格。 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 #include <tabulate/table.hpp> using namespace tabulate; int main() { Table table; table.add_row({"Company", "Contact", "Country"}); table.add_row({"Alfreds Futterkiste", "Maria Anders", "Germany"}); table.add_row({"Centro comercial Moctezuma", "Francisco Chang", "Mexico"}); table.add_row({"Ernst Handel", "Roland Mendel", "Austria"}); table.add_row({"Island Trading", "Helen Bennett", "UK"}); table.add_row({"Laughing Bacchus Winecellars", "Yoshi Tannamuri", "Canada"}); table.add_row({"Magazzini Alimentari Riuniti", "Giovanni Rovelli", "Italy"}); // 设置每一行的宽度 table.column(0).format().width(40); table.column(1).format().width(30); table.column(2).format().width(30); // 遍历第一行中的单元格 for (auto& cell : table[0]) { cell.format() .font_style({fonttyle::underline}) .font_align(FontAlign::center); } // 遍历第一列中的单元格 for (auto& cell : table.column(0)) { if (cell.get_text() != "Company") { cell.format() .font_align(FontAlign::right); } } // 遍历表格中的行 size_t index = 0; for (auto& row : table) { row.format() .font_style({fonttyle::bold}); // 轮流把整行的背景设为蓝色 if (index > 0 && index % 2 == 0) { for (auto& cell : row) { cell.format() .font_background_color(Color::blue); } } index += 1; } std::cout << table << std::endl; } 在 tabulate 中嵌套表格很容易,因为 Table.add_row(...) 这个函数可以接受 std::string 类型和 tabulate::Table。下面是一个嵌套表格的例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include "tabulate/table.hpp" using namespace tabulate; using namespace std; int main() { Table hellogithub; hellogithub.add_row({"HelloGitHub"}); hellogithub[0][0] .format() .font_background_color(Color::blue) .font_align(FontAlign::center); Table hglink; hglink.add_row({"GitHub repo", "Website"}); hglink.add_row({"github.com/521xueweihan/HelloGitHub", "hellogithub.com"}); hellogithub.add_row({hglink}); // 嵌套! cout << hellogithub << endl; return 0; } 0x41 Markdown 可以使用 MarkdownExporter 来将一个表格导出为 GFM 风格的 Markdown。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include "tabulate/markdown_exporter.hpp" // 注意这个头文件 #include "tabulate/table.hpp" using namespace tabulate; using namespace std; int main() { Table hellogithub; hellogithub.add_row({"HelloGitHub"}); hellogithub[0][0].format().font_style({fonttyle::bold}); // 加粗样式,在 Markdown 中可以表现出来 hellogithub.add_row({"GitHub repo: github.com/521xueweihan/HelloGitHub"}); hellogithub.add_row({"Website: hellogithub.com"}); // 导出为 Markdown MarkdownExporter exporter; auto markdown = exporter.dump(hellogithub); cout << hellogithub << endl << endl; cout << "Markdown Source:\n\n" << markdown << endl; return 0; } 导出效果如下: HelloGitHub GitHub repo: github.com/521xueweihan/HelloGitHub Website: hellogithub.com 注意:Markdown 不能指定每一个单元格的对齐样式,只能指定一列的对齐样式,像这样 hg.column(1).format().font_align(FontAlign::center);。 如果想要更详细地了解 tabulate 的用法,请查看官方文档 https://github.com/p-ranav/tabulate 。 本文是作者的第一次关于此类型文章的尝试,如有不足之处,请指正,谢谢! 再见!

2020/2/21
articleCard.readMore