2024.1.21 ~ 2024.2.2 集训总结
集训大纲
- Week1:
- 图论:拓扑排序、欧拉回路、二分图、最小生成树
- 数据结构:并查集、堆、单调队列
- week2:
- 图论:连通性
- 数据结构:线段树
图论
拓扑排序
将 DAG 上的点以关联性进行排序,得到一个有关联的枚举顺序。
有了这种特别的枚举顺序,使得在 DAG 上 DP 的转移过程更加合理且有序。
所以,DAG
另类排序顺序
Eg. 菜肴制作
要求在满足拓扑序的条件下,小编号的尽可能靠前。
小编号靠前 = 大编号靠后
不妨倒序考虑,先将大编号的向前放,最后倒序输出。
那么由于要让原本在后的在前,相当于反转拓扑序,所以建反图,然后满足大编号向前,即字典序越大越好,所以将队列改成大根堆即可。
数量关系建图
Eg. 数列恢复
有一个长度为
的整数数列 。 定义
。 你现在只知道所有
的符号,请恢复一个合法的数列,或指出不存在这样的数列。
那么已知
通过大小关系建图,
那么
既然
最后每个
丑陋代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100 + 10;
int n;
int f[N];
char s[N][N];
vector<int> G[N], g[N];
int find (int x) {
if (x == f[x]) return x;
return f[x] = find(f[x]);
}
int in[N], a[N];
map<int, int> mp;
signed main() {
ios::sync_with_stdio(0);
while (cin >> n) {
mp.clear();
for (int i = 0; i <= n; i ++) f[i] = i, G[i].clear(), g[i].clear(), a[i] = 0, in[i] = 0;
for (int i = 1; i <= n; i ++) {
for (int j = i; j <= n; j ++) {
cin >> s[i][j];
if (s[i][j] == '+') {
G[i - 1].push_back(j);
} else if (s[i][j] == '-') {
G[j].push_back(i - 1);
} else {
f[find(j)] = find(i - 1);
}
}
}
for (int i = 0; i <= n; i ++) {
for (auto u : G[i]) {
g[find(i)].push_back(find(u)), in[find(u)] ++;
}
}
queue<int> q;
int tot = 0, idx = 0;
for (int i = 0; i <= n; i ++) {
if (!mp[find(i)]) {
if (!in[find(i)]) q.push(find(i));
idx ++;
mp[find(i)] = 1;
}
}
while (!q.empty()) {
int u = q.front();
q.pop(), tot ++;
for (auto v : g[u]) {
a[v] = max(a[v], a[u] + 1);
if(!(-- in[v])) {
q.push(v);
}
}
}
if(tot != idx) cout << "NO\n";
else {
cout << "YES\n";
for (int i = 1; i <= n; i ++) {
cout << a[find(i)] - a[find(i - 1 )] << ' ';
}
cout << endl;
}
}
return 0;
}
说实话,如果不知道算法是拓扑排序,这道题我绝对不知道要往图论方向想。
欧拉回路
这个知识点还是很陌生的,毕竟 GG 只给我们做了板题
定义
一条经过了图
特别的,当起点与终点相同时,称为欧拉回路。
判别
大前提:该图弱联通(有向)或联通(无向)
定义:
分别表示有向图中一个点的入度和出度, 表示一个无向图中的一个点的度数。
- 有向图
当且仅当所有点 时,该有向图存在欧拉回路。
当且仅当除起点和终点以外的点 ,且 ,该有向图存在欧拉路径。 - 无向图
当且仅当该图仅有 个奇点,该图存在欧拉回路。
当且仅当该图仅有 个奇点,该图存在欧拉路径。
寻找
从奇点(欧拉路径)或任意一点(欧拉回路)开始 DFS,每次经过一条边就将其删去(保证每一条边仅经过一次),然后以后序输出。
对于欧拉回路,显然这样输出实际上即为遍历顺序,并且每次回溯前一定已经将该点可达的所有环全部走过;但对于欧拉路径,遍历过程中可能会在走一部分环后走到终点并且无法继续,此时会出现回溯并重新遍历路径上尚未遍历的环,也就是方案不能仅仅按照遍历顺序。因此这时按照后序输出,就相当于首先保证遍历所有环然后再考虑剩余的路径,从而保证了正确性。
实现技巧:
- 删边:可以使用链前,由于每次存边时,正边和反边的编号
即可得到另一边的编号,所以每次删除 和 号边即可。
考察方向:
建模
-
宝石装箱
有
颗宝石,价值分别为 。
宝石的颜色共有 种,每种颜色恰好有 颗宝石。
现在要将宝石装进两个箱子,满足以下两个条件:- 两个箱子的宝石总价值相等;
- 每个箱子每种颜色恰有
颗宝石。
请你设计一种划分方案,数据保证一定存在符合条件的划分方案。
条件 1 很好满足,每次选取
的宝石放入一个箱子中, 放入另一个箱子中即可。现在关键在于要满足条件 2。
对于每组
将其视作边,该图一定存在欧拉回路:每种颜色必定度数为 。又因为刚才满足条件 时的奇偶分组,所以求出该图的欧拉回路之后,奇偶间隔取边所对应的 即可。
构造
-
最少路径覆盖问题
要求:简单路径,每条路径不能重复
由于欧拉路径是保证不重复经过每一条边的,所以可以尝试用欧拉路径去覆盖这张图。
若该图没有奇点,
条欧拉路径即可覆盖完整个图。若该图奇点数
,那么该图会长这样:人类智慧
由于握手定理,奇点数量为偶数个。
若要使其被多条欧拉路径所覆盖,可以先将两两奇点连边,使得该图为欧拉图,然后找出该图的欧拉路径,删除其中的虚边,即为覆盖完该图需要的欧拉路径条数。
所以最后答案即为 :
。NKOJ P8436 守卫王国
#include <vector> #include <iostream> #include <stack> #include <cstring> using namespace std; #define endl '\n' const int N = 5e5 + 10; int n, m; struct edge { int to, nxt, id; }e[N]; int h[N], idx, tmp; void add (int u, int v, int id) { e[idx] = edge{v, h[u], id}, h[u] = idx, idx ++; e[idx] = edge{u, h[v], - id}, h[v] = idx, idx ++; } vector<int> col[N]; int bel[N], tot; int vis[N]; void findltk (int u) { col[tot].push_back(u), bel[u] = tot; for (int i = h[u]; ~i ; i = e[i].nxt) { if(!bel[e[i].to]) findltk(e[i].to); } } int d[N]; bool mark[N]; stack<int> ans; void dfs (int u) { for (int i = h[u]; ~i; i = e[i].nxt) { if(mark[i]) continue; h[u] = e[i].nxt; mark[i] = mark[i ^ 1] = 1; dfs(e[i].to); ans.push(e[i].id); } } signed main () { ios::sync_with_stdio(0); cin >> n >> m; memset(h, -1, sizeof h); while(m --) { int u, v; cin >> u >> v; add(u, v, ++ tmp), d[u] ++, d[v] ++; } for (int i = 1; i <= n; i ++) { if(! bel[i]) ++ tot, findltk(i); } int num = 0; for (int i = 1; i <= tot; i ++) { int cnt = 0; if(col[i].size() == 1) continue; for (auto u : col[i]) { if(d[u] & 1) cnt ++; } num += max(cnt / 2, 1); // 一条链删除两个奇点 => ans >= sum(max(cnt / 2, 1)) // 证明: 构造证明 } cout << num << endl; for (int i = 1; i <= tot; i ++) { if(col[i].size() == 1) continue; int cnt = 0; vector<int> odd; for (auto u : col[i]) { if(d[u] & 1) cnt ++, odd.push_back(u); } if(cnt == 0) { dfs(col[i][0]); cout << ans.size() << ' '; while(!ans.empty()) cout << ans.top() << ' ', ans.pop(); cout << endl; } else { for (int j = 0; j < odd.size(); j += 2) { if(cnt == 2) break; add(odd[j], odd[j + 1], 0); d[odd[j]] ++, d[odd[j + 1]] ++, cnt -= 2; } int st; for (auto u : col[i]) if(d[u] & 1) st = u; dfs(st); while(1) { vector<int> ve; while(!ans.empty() && ans.top() != 0) ve.push_back(ans.top()), ans.pop(); cout << ve.size() << ' '; for (auto v : ve) cout << v << ' '; cout << endl; if(ans.size()) ans.pop(); if(ans.empty()) break; } } } return 0; }
生成树
常用算法:
- kruskal (加边、
) - prim (加点、
、适合完全图)
考察方向
生成树上操作
一般和数据结构绑在一起,在求出最小生成树后,根据最小生成树的性质,求解问题。
-
最小生成树边分类
判断每条边是一定出现在最小生成树上还是可能还是不可能
首先先将任意一颗最小生成树找出来,然后讨论每条非树边。
- 如果该边连接的两点在生成树上的边中存在边长与其相等的边,那么该边可以替换这些与其边长相等的边,此时用倍增标记所有与其相等的边,它们也不是唯一的了。
- 否则,这条边一定不存在最小生成树上。
-
次小生成树
首先,次小生成树有一个重要的性质:它与最小生成树的差别只有一条边。证明:
假设将边 和 均改为 其的边 ,可以得到次小生成树。
那么,记除去 的边权和为 ,即
即
又因为 所以后边的书均大于等于 ,那么只加一个肯定小于等于加两个,所以只改一条边更优。
那么,对于每一条非树边,该边连接的两点在生成树上的边中找出以该边替换后增量最小的,即路径最大值,然后取每条非树边替换后结果的最小值即可。同样的,路径最大值也可以使用倍增维护。
建模
- 跨越雷区
一个点与两个雷区距离的最小值最大化,即这两个雷区距离 ,所以为了让与雷区距离最大值最小化,不妨连接所有雷区,然后找出最小生成树上最大边,即为路径上最大值距离 ,答案 即可。(注意任意雷区可互达,所以是完全图,使用 Prim 算法)
连通性
最关键的:DFS 树
强连通分量
定义不再赘述(虽然上一次总结的时候也没写)。
在处理有环有向图的问题时,如果缩点后每个强连通分量里的信息可以方便处理出来的化,不妨先缩点,然后处理强连通分量内部信息,再在新的 DAG 上,进行拓扑排序、最短路……化繁为简。
双连通分量
该知识点掌握不牢,很多题不会做
性质
边双和点双缩点后均得到一棵树
- 点双
- 每个
的点双中,每一条边都在一个简单环中,特别的,当点数 边数时,该点双中的所有边都仅在一个简单环中。 - 点双中任意两个点都存在两条经过点不相同的路径。
- 点双不具有传递性。
- 非割点仅属于一个点双中。
- 每条边仅属于一个点双中。
- 每个
- 边双
边双联通即两点间没有必经边。- 边双联通具有传递性,
和 边双联通,那么 边双联通。 - 对于边双内任意一条边
,存在经过 的回路。 - 对于边双内任意一点
,存在经过 的回路。
解题
如果看到题目中出现:删去某一点,删去某一边,可以往点双或边双的方向想。
az……真写不出来了。
DS
DSU
维护许多树形集合的一种数据结构,复杂度接近常数。
支持操作:
- 合并
- 删除(需要建虚点)
优化:
- 路径压缩(几乎必备)
- 按秩合并
难点:
本体并不算很难,难在运用和建模,一般维护父子结构时可以使用。
单调队列
在
同样,本体没什么花样,一般用于优化 DP。
这里重点说优化 DP。
对于形如
线段树
本体可以出得很花,运用也可以很广。
本体
一些区间操作,区间查询的裸题,专门考验对线段树的熟练程度。
可能出询问的东西很奇怪,但是可以线段树区间合并来合并出答案的问题。
运用
好像我真的不会用
可以优化 DP,即区间 updata(dp[i])
原值即可。
其它的真的不会了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)