·听了一天的浑浑噩噩
·这个老师近距离看有点精致可爱,然鹅他过于强
·老师经典语句:
“来我们看一道简单题”,然鹅,是蓝题
“来我们再来看一道题",然鹅,是紫题
tql!!!tjl!!!%%%%%
一、图的入门介绍
·什么是图?——G(graph)=(V(点),E(边))
把图进行赋值,所赋值即为权值——点权,边权.
·图的储存
edge:next,to.
eg:x——>y
next:下一个以x为开头的边在数组的位置
to:y
first:以x为开头的第一条边
储存方法:找到最后,在往前倒
板子:
struct edge { int next, to; edge() {} edge(int _next, int _to) : next(_next), to(_to) {} } e[M]; void add_edge(int x, int y) { e[++tot] = edge(first[x], y); first[x] = tot; e[++tot] = edge(first[y], x); first[y] = tot; } int main() { for (int x = first[p]; x; x = e[x].next) { //find linkers of point p q = e[x].to; } }
升级版板子(带stl):
#include <vector> vector<int> to[N]; to[x].push_back(y); for (int x = 0; x < to[p].size(); ++x) { q = to[p][x] }
·例题:
联合权值https://www.luogu.org/problem/P1351
以下为此题温馨提示(dl可自行忽略):
(x,y), dis[x][y]=2.
以s为中心对答案的贡献:
ab+bc+ca+cd+bd+cd
=[(a+b+c+d)²-(a²+b²+c²+d²)]/2.
·相关补充内容:
1、团(完全图)
有n个点,点之间两两都有边,共有n(n-1)/2条边.
【例题:
给定3n个点,保证有一个大小为2n的团.
求任意一个大小为n的团.
——方法:
3n-2n=n.故最多有n个不在团中的点—故删除2n个包括n个不在团中的点即可。
每次找到一个没有连边的点对(x, y),把他们两个都删了.
删n次剩下的就是一个大小为n的团.】
2、图的退化—>树
图:n个点,m条边
树:n个点,n-1条边,连通
3、dfs序:
顾名思义,dfs序就是dfs的顺序,分两种。
第一种:处理子树问题.
即为 1,2,5,6,3,7,8,12,13,4,9,10,11
第一种dfs序中每个子树可以看成dfs序中的一段连续区间.
子树问题 ? 序列问题
第二种:处理树链问题.
即为 1,2,5,-5,6,-6,-2,3,7,-7,8,12,-12,13,-13,-8,-3,4,9,-9,10,-10,11,-11,-4,-1。
求[1,2,5,4,11]—合并[1,2,5,-5,6,-6,-2,3,7,-7,8,12,-12,13,-13,-8,-3,4,9,-9,10,-10,11]得到[1,4,11]
合并 [1,2,5],由此得到[1,2,5,4,11].
第二种dfs序中每个树链可以看成dfs序中的一段连续区间.
树链问题 ? 序列问题.
4、取反图
·遍历所有头节点,方向取反.
·直接上板子:
二、最短路
·源点s:起点,汇点T:终点.
·1、单源最短路:一个点为起点,所有点为终点.
2、多源最短路:所有点为起点,所有点为终点.
·求最短路算法:
1、多源:Bellmen Ford (spfa), Floyd
2、单源:Dijkstra
三、flyod
·这是最简单的一个!!!!!!!!!!!
·多源最短路,所有点到所有点。
·动态规划.
·F[i][j][k]表示从i到j的最短路,其中经过的点只有1, 2, 3, …, k, i, j这些.
F[i][j][k] = min(F[i][j][k – 1], F[i][k][k – 1] + F[k][j][k – 1]).
显然k这一维可以省略.
F[i][j] = min(F[i][j], F[i][k] + F[k][j]).
k从1到n遍历一遍就好了.
时间复杂度:O(N^3).
·初始值:没给的边赋值为∞
·板子:
void floyd() { for (int k = 1; k <= n; ++k) 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]); }
四、dijkstra
·单源最短路,一到多.
·数组dis[],dis[p]表示源点S到点i的最短路径长度.(暂时)
集合T,p∈T表示源点S到点p的最短路径长度已经被求出来了.
·初始化:一开始只有S在T中,除了dis[S] = 0,别的dis = inf.
·贪心思想:每次找到不在T中的所有点中,dis最小的点p,把p丢进T,然后更新所有和p相邻的dis值.
重复n - 1遍上述操作,就求出了n个所有点的最短路.
·考虑先后更新的两个点v1, v2.
由dijkstra的步骤知道,dis[v1] < dis[v2].
而S到v2的前一步,一定经过一个小于dis[v2]的点v.
我们知道这所有的v都已经在v2前已经求出来了,所以的确可以求出来正确的dis[v2].
证明也同时说明,dijkstra算法要求所有的边边权都是正的.
时间复杂度:O(n^2).
堆优化T ? O(nlogn).
·限制条件:所有边的边权为正.
·板子:
struct edges { int next, to, v; edges() {} edges(int _n, int _t, int _v) : next(_n), to(_t), v(_v) {} } e[]; struct heap_node { int v, to; heap_node() {} heap_node(int _v, int _to) : v(_v), to(_to) {} }; inline bool operator < (const heap_node &a, const heap_node &b) { return a.v > b.v; //这里实现的是小根堆,如果要大根堆,把 > 改成 < 即可 } priority_queue <heap_node> h; inline void add_to_heap(const int p) { for (int x = first[p]; x; x = e[x].next) if (dis[e[x].to] == -1) h.push(heap_node(e[x].v + dis[p], e[x].to)); } void Dijkstra(int S) { memset(dis, -1, sizeof(dis)); while (!h.empty()) h.pop(); dis[S] = 0, add_to_heap(S); int p; while (!h.empty()) { if (dis[h.top().to] != -1) { h.pop(); continue; } p = h.top().to; dis[p] = h.top().v; h.pop(); add_to_heap(p); } }
五、Bellman Ford(spfa)
·spfa = 队列优化bellman ford.
·初始化:一开始dis[S] = 0, 对于其他的p,dis[p] = inf.
维护一个队列,队列里面存是dis被更新的点.
·每次取出队首,用队首的dis信息更新和它相连的其他点的dis,然后把这些点不在队列里且dis值变小的重新入队.
队列为空表示没有要更新的点,算法结束.
·每次进队列的点都是dis值更新的点.
所以这个点的dis已经最小的话,就不会更新,也就是不会再进队了.
同时这个点的dis能达到最小值,否则它的最短路径上的前一个点的dis也不是最小值,以此类推,所有最小值路径上的dis都不是最小值,也即dis[S] > 0,这显然不对.
·理论复杂度O(NM).
·N是点数,M是边数.
·实际跑起来很快,但是会被特定结构的图卡到理论上界.
e.g. 奶牛题专卡spfa.
一般建议不写.
·SLF优化:
spfa有时候会被卡,这时候适当地添加SLF优化可以不被卡.
SLF优化在特定图下还是会被卡,不过被卡的几率很小.
SLF优化的地方就是每次更新dis进队时,判断一下是不是当前点的dis值比队头还小:如果是的话,直接把当前点插入队头.
只需要加一句if就可以了,很方便而且不容易被卡.
·板子:
纯spfa:
struct edge { int next, to, v; }e[]; void spfa(int S) { int p, x, y, l, r; for (x = 1; x <= n; ++x) dis[x] = inf; q[0] = S, dis[S] = 0, v[S] = 1; for (l = r = 0; l != (r + 1) % N; ) { p = q[l], ++l %= N; for (x = first[p]; x; x = e[x].next) if (dis[p] + e[x].v < dis[(y = e[x].to)]) { dis[y] = dis[p] + e[x].v; if (!v[y]) { v[y] = 1; q[++r %= N] = y; } } v[p] = 0; } }
带slf优化的spfa:
struct edge {
int next, to, v;
}e[];
int inc(int x) {
x = x + 1;
x = x % N;
return x;
}
int dec(int x) {
x = x - 1 + N;
x = x % N;
return x;
}
void spfa(int S) {
int p, x, y, l, r;
for (x = 1; x <= n; ++x)
dis[x] = inf;
q[0] = S, dis[S] = 0, v[S] = 1;
for (l = r = 0; l != (r + 1) % N; ) {
p = q[l], inc(l);
for (x = first[p]; x; x = e[x].next)
if (dis[p] + e[x].v < dis[(y = e[x].to)]) {
dis[y] = dis[p] + e[x].v;
if (!v[y]) {
v[y] = 1;
if (dis[y] < dis[q[l]]) q[dec(l)] = y; //SLF
else q[inc(r)] = y;
}
}
v[p] = 0;
}
}
·TIPS:求最短路可用bfs,不要用dfs(否则会爆时)
·相关补充内容:
出边:出去;入边:进入。
出度:往外指的边的条数;入度:指向它的边的条数。
六、差分约束系统
·一组二元不等式.
·题面:
x[i] – x[j] ≤ k.
求x[i] – x[0]的最小值.
·对一个x[i] – x[j] ≤ k:
j指向i有一条长度为k的边.
求以0为起点的最短路.
·对照:
·什么时候无解?
---有负环的时候.
eg:x[2]-x[1]<=-1
x[1]-x[2]<=-1
相加,0<=-2,显然不成立,故无解.
·如果把小于号改成大于号,求最小值呢?
---最长路.
·如果有一部分是大于号并且求最大值呢?
---把大于号改成小于号并求最短路.
七、二分图匹配
·二分图:两部分:一部分点集为x、y,x、y中两两不相连.
·匹配:左边点与右边点连成一条边。
二分图匹配:能连多少边且每个点至多被连了一次。
·算法:匈牙利算法:介于当前匹配情况去搜有没有更大的匹配,找到一个之前未匹配的点.
找到2个点,2/2=1,3—>4。
每次加新边,右边只能dfs一次.
贪心的思想.
每次找一条路径,是非匹配-匹配相互交替的.
如果找到一个2k长度的路径,说明有一个更大的匹配方式.
每次找路径都是暴力搜,所以每次寻找路径都要搜满m条边.
对每个点都要搜一次,所以总时间复杂度是O(NM).
·板子:
int find(int x) { for (int i = 1; i <= n; ++i) if (a[x][i] == 1 && flag[i] == 0) { flag[i] = 1; if (pair[i] == 0 || find(pair[i])) { pair[i] = x; return 1; } } return 0; } int edmonds() { int cnt = 0; memset(pair, 0, sizeof(pair)); for (int i = 1; i <= n; ++i) { memset(flag, 0, sizeof(flag)); if (find(i)) ++cnt; } return cnt; }
八、Kruskal,最小生成树
·生成树:(对不起我也解释不好请自行意会)就是在原树上生成的一棵小树.
·最小生成树:边权最小的生成树.
·一般在无向图上做.
·最小生成树的性质:对于一条不在最小生成树上的边,它与最小生成树形成了一个环,那么这个环上这条边的权值是最大的之一.
做题基本要用到这个大佬的性质.
· 最小生成树不是唯一的,往往存在多个。
·算法:Kruskal
把所有边权从小到大排序,依次检查每一条边是否在最小生成树上.
两种case:
1. 边两边的点本来已经在同一棵树中了,那么会形成一个环.
2. 边两边的点不来不在一棵树中,那么会把两棵树变成一棵.
对于case1,显然环上其他的边权值都不大于它,也就是说它是环上权值最大的边之一,故它不应该在最小生成树上.
对于case2,直接把这条边加入最小生成树就可以了.
·怎么判断两个点在不在同一棵树里?
并查集!
一开始每个点都在一个独立的集合里.
每次合并两棵树 ? 集合并.
查询插入边有没有成环 ? 集合查询.
并查集作用:检查连通性。
两点不连通—>加边。
两点原来连通—>不加边。
把所有点都加一遍。
·最小生成树:从小往大加边。
最大生成树:从大往小加边。
·板子:
struct edges{ int from, to, v; edges() {} edges(int a, int b, int c) : from(a), to(b), v(c) {} }e[]; inline bool operator <(const edges &a, const edges &b){ return a.v < b.v; //如果是最大生成树,则把 < 改成 > 即可 } inline void add_edge(int X, int Y, int Z){ e[++tot] = edges(X, Y, Z); } int find_fa(int x){ return x == fa[x] ? x : fa[x] = find_fa(fa[x]); } int Kruskal(){ sort(e + 1, e + tot + 1); int i; for (i = 1; i <= n; ++i) fa[i] = i; int fax, fay, cnt = n - 1, res = 0; for (i = 1; i <= tot; ++i){ fax = find_fa(e[i].from), fay = find_fa(e[i].to); if (fax != fay){ --cnt; fa[fax] = fay; res += e[i].v; if (!cnt) return res; } } return -1; }
·相关补充内容:
次大生成树。
例题:Bzoj1977次小生成树Tree
-严格次小生成树:当前次小生成树边权和>最小生成树边权和.
改一条边。
-做法:最小生成树+LCA
先做出一个MST.
对每一条非树边边,加入MST后会形成一个环.
删除环上的除了这条非树边的最大值就好了.
但是权值可能相同,所以还要记录一下严格次大边是多少.
-板子:
#include <cstdio> #include <algorithm> using namespace std; typedef long long ll; const int N = 100001; const int M = 300001; struct data{ int x, y, v; bool selected; }a[M]; struct edge{ int next, to ,v; }e[N * 2]; struct tree_node{ int dep, fa[17], d1[17], d2[17]; }tr[N]; inline bool operator < (const data a, const data b){ return a.v < b.v; } int n, m, cnt, tot, del = 1e9; int first[N], fa[N]; ll ans; inline int read(){ int x = 0, sgn = 1; char ch = getchar(); while (ch < ‘0‘ || ch > ‘9‘){ if (ch == ‘-‘) sgn = -1; ch = getchar(); } while (ch >= ‘0‘ && ch <= ‘9‘){ x = x * 10 + ch - ‘0‘; ch = getchar(); } return sgn * x; } inline void add_edge(int x, int y, int z){ e[++tot].next = first[x], first[x] = tot; e[tot].to = y, e[tot].v = z; } void add_Edges(int X, int Y, int Z){ add_edge(X, Y, Z); add_edge(Y, X, Z); } int find_fa(int x){ return x == fa[x] ? x : fa[x] = find_fa(fa[x]); } void dfs(int p){ int i, x, y, FA; for (i = 1; i <= 16; ++i){ if (tr[p].dep < (1 << i)) break; FA = tr[p].fa[i - 1]; tr[p].fa[i] = tr[FA].fa[i - 1]; tr[p].d1[i] = max(tr[p].d1[i - 1], tr[FA].d1[i - 1]); if (tr[p].d1[i - 1] == tr[FA].d1[i - 1]) tr[p].d2[i] = max(tr[p].d2[i - 1], tr[FA].d2[i - 1]); else { tr[p].d2[i] = min(tr[p].d1[i - 1], tr[FA].d1[i - 1]); tr[p].d2[i] = max(tr[p].d2[i - 1], tr[p].d2[i]); tr[p].d2[i] = max(tr[p].d2[i], tr[FA].d2[i - 1]); } } for (x = first[p]; x; x = e[x].next) if ((y = e[x].to) != tr[p].fa[0]){ tr[y].fa[0] = p, tr[y].d1[0] = e[x].v, tr[y].dep = tr[p].dep + 1; dfs(y); } } inline int lca(int x, int y){ if (tr[x].dep < tr[y].dep) swap(x, y); int tmp = tr[x].dep - tr[y].dep, i; for (i = 0; i <= 16; ++i) if ((1 << i) & tmp) x = tr[x].fa[i]; for (i = 16; i >= 0; --i) if (tr[x].fa[i] != tr[y].fa[i]) x = tr[x].fa[i], y = tr[y].fa[i]; return x == y ? x : tr[x].fa[0]; } void calc(int x, int f, int v){ int mx1 = 0, mx2 = 0, tmp = tr[x].dep - tr[f].dep, i; for (i = 0; i <= 16; ++i) if ((1 << i) & tmp){ if (tr[x].d1[i] > mx1) mx2 = mx1, mx1 = tr[x].d1[i]; mx2 = max(mx2, tr[x].d2[i]); x = tr[x].fa[i]; } del = min(del, mx1 == v ? v - mx2 : v - mx1); } void work(int t, int v){ int x = a[t].x, y = a[t].y, f = lca(x, y); calc(x, f, v); calc(y, f, v); } int main(){ n = read(), m = read(); int i, f1, f2, TOT = 0; for (i = 1; i <= m; ++i) a[i].x = read(), a[i].y = read(), a[i].v = read(); for (i = 1; i <= n; ++i) fa[i] = i; sort(a + 1, a + m + 1); for (i = 1; i <= m; ++i) if ((f1 = find_fa(a[i].x)) != (f2 = find_fa(a[i].y))){ fa[f1] = f2; ans += a[i].v; a[i].selected = 1; add_Edges(a[i].x, a[i].y, a[i].v); ++TOT; if (TOT == n - 1) break; } dfs(1); for (i = 1; i <= m; ++i) if (!a[i].selected) work(i, a[i].v); printf("%lld\n", ans + del); return 0; }
Ps/ h[][]记录严格次大边,G[][]记录最大边
在处理时,如上述代码,a、b边可分开处理。
原文:https://www.cnblogs.com/konglingyi/p/11384786.html