图论 从入门到____________
图论
特殊图:
-
没有环的无向图 : 树(图连通) 森林(不连通)
-
\(有向树\begin{cases} 外向树 \to 根节点向外延伸\\ 内向树 \to 从叶子节点指向根 \end{cases}\)
-
章鱼图(基环树) : 无向联通,内有一个环 \(\to\) 断环成树
-
\(仙人掌\begin{cases} 边仙人掌 \to 每条边最多在一个环上\\ 点仙人掌\to 每个点最多在一个环上 \\ 点仙人掌 一定是边仙人掌 \\ \end{cases}\)
-
二分图 : 树也为二分图,按深度奇偶分开即可
判断二分图的方法: 黑白染色法(不存在环)
充要条件 : 二分图一定没有奇环
最短路
- 单源最短路 : 一点到多点
- 多源最短路 ;多点到多点
- 差分约束
\(x_i - x_j >= c\)
\(\Rightarrow{x_j - x_i <= -c}\)
\(\Rightarrow{add(i,j,-c)}\)
\(x_i - x_j <= c\)
\(\Rightarrow{x_i - x_j <= c}\)
\(\Rightarrow{add(j,i,c)}\)
\(x_i = x_j\)
\(\Rightarrow{x_i - x_j <= 0,~~~ x_j - x_i <= 0}\)
\(\Rightarrow{add(j,i,0),~~~ add(i,j,0)}\)
把\(x_?\)移到同一边,后面的未知数的下标向前面未知数的下标连边,边权为不等式后的值
生成树
- prim板子
void prim() {
q.push((node){s,0});
while(!q.empty()) {
node tp = q.fr;
if(vis[tp.po]) continue;
vis[tp.po] = true;
q.pop() ;
for(int i = head[tp.po] ;i ; i = e[i].net) {
int to = e[i].to;
if(dis[to] > e[i].dis) {
dis[to] = e[i].dis;
if(!vis[to]) q.push((node){to,e[i].dis});
}
}
}
}
-
并查集?
-
路径压缩
int findf(int x) {
return f[x] == x ? f[x] = findf(f[x]);
}
- 按置合并
void hb(int xx ,int yy){
int x = findf(xx) ,y = fidnf(yy);
if(dep[x] > dep[y])
f[y] = x;
else f[x] = y;
if(dep[x] == dep[y]) dep[y]++;
}
严格次小生成树
- 首先可以发现次小生成具有一定的性质:
我们如果要构造次小生成树,我们就要,尽可能的删去树边中的一个最大值,
然后维护一个非树边中的最小值,这个最小值要比树边在最大值大,但是比树边的最大值要大- 怎样做到\(nlog(n)\)的的时间复杂度? -- 倍增即可
- 我们加入一条非树边使得这棵树出现了一个环,所以,我们要断环
- 可以预先 DFS 处理这棵树的点深度,每个点的父亲,最大值和次大值的初始值
再使用倍增求出每个点所能到达的最大值和次大值
然后找出一颗最小生成树,统计它的权值和,并对于每条处于最小生成树中的边打一个标记
对于每条不在最小生成树中的边,找出与它相对的处于最小生成树中的最大的一条边,删去,然后一边跳一边枚举路径上的边权
然后判断当前枚举边与删去边的权值大小,若相同加入当前边的次大值,否则加入最大值,然后统计权值
最后把统计出的所有权值取一个最小值就是严格次小生成树的权值
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define int long long
using namespace std;
const int N = 1e5 + 100;
const long long inf = 21474836470000;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int n, sum,m;
bool used[N];
namespace SLV {
int maxx[N][25], slv[N][25];
struct Edge {
int from, to, dis, net;
bool operator<(const Edge& b) const { return dis < b.dis; }
} e[N << 2];
int head[N], nume;
void add_edge(int from, int to, int dis) {
e[++nume].from = from, e[nume].to = to, e[nume].dis = dis;
e[nume].net = head[from], head[from] = nume;
}
int fa[N][25], dep[N];
void dfs(int x, int fath) {
fa[x][0] = fath, dep[x] = dep[fath] + 1ll;
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (to == fath)
continue;
maxx[to][0] = e[i].dis;
slv[to][0] = -inf;
dfs(to, x);
}
}
void pre() {
for (int i = 1; i <= 18; ++i) {
for (int j = 1; j <= n; ++j) {
fa[j][i] = fa[fa[j][i - 1]][i - 1];
maxx[j][i] = max(maxx[j][i - 1], maxx[fa[j][i - 1]][i - 1]);
slv[j][i] = max(slv[j][i - 1], slv[fa[j][i - 1]][i - 1]);
if (maxx[j][i - 1] > maxx[fa[j][i - 1]][i - 1])
slv[j][i] = max(slv[j][i], maxx[fa[j][i - 1]][i - 1]);
else if (maxx[j][i - 1] < maxx[fa[j][i - 1]][i - 1])
slv[j][i] = max(slv[j][i], maxx[j][i - 1]);
}
}
}
int lca(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 18; i >= 0; i--) {
if (dep[fa[x][i]] >= dep[y])
x = fa[x][i];
}
if (x == y)
return x;
for (int i = 18; i >= 0; i--)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
int q_max(int x, int y, int dis) {
int ans = -inf;
for (int i = 18; i >= 0; i--) {
if (dep[fa[x][i]] >= dep[y]) {
if (dis != maxx[x][i])
ans = max(ans, maxx[x][i]);
else
ans = max(ans, slv[x][i]);
x = fa[x][i];
}
}
return ans;
}
}
namespace Kur {
int nume, head[N];
struct Edge {
int from, to, dis, net;
bool operator<(const Edge& b) const { return dis < b.dis; }
} e[N << 2];
void add_edge(int from, int to, int dis) {
e[++nume].from = from, e[nume].to = to, e[nume].dis = dis;
e[nume].net = head[from], head[from] = nume;
}
int f[N];
int findf(int x) {
return f[x] == x ? x : f[x] = findf(f[x]);
}
void kur() {
for (int i = 1; i <= n; ++i) f[i] = i;
sort(e + 1, e + nume + 1);
for (int i = 1; i <= nume; ++i) {
int x = findf(e[i].from), y = findf(e[i].to);
if (x != y) {
f[x] = y;
sum += e[i].dis;
SLV::add_edge(e[i].from, e[i].to, e[i].dis);
SLV::add_edge(e[i].to, e[i].from, e[i].dis);
used[i] = 1;
}
}
}
int solve() {
int ans = inf;
for (int i = 1; i <= nume; i++) {
if (used[i]) continue;
int Lca = SLV::lca(e[i].from, e[i].to);
int maxfrom = SLV::q_max(e[i].from, Lca, e[i].dis);
int maxto = SLV::q_max(e[i].to, Lca, e[i].dis);
ans = min(ans, sum - max(maxfrom, maxto) + e[i].dis);
}
return ans;
}
}
signed main() {
n = read(), m = read();
for (int i = 1, u, v, w; i <= m; ++i) {
u = read(), v = read(), w = read();
Kur::add_edge(u, v, w);
}
Kur::kur();
SLV::slv[1][0] = -inf;
SLV::dfs(1, 0);
SLV::pre();
printf("%lld",Kur::solve());
system("pause");
return 0;
}
solution:
考虑要求的最小生成树与原图的最小生成树的关系,可以发现,这与次小生成树类似,同样可能考虑加边断环的问题,只不过要考虑的问题更多,
分情况讨论
- 减小的边为树边,那就找\(c\)最小的那条边,反正可以变为负边,一个劲的减就行
- 减小的边为非树边,那就类似于加边断环求解,判断加这条边和加到树边哪种情况更优秀
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>
#define int long long
using namespace std;
const int N = 2e5 + 100;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int n, m;
int val, s, pre_jia, pre_jian, flag;
bool vis[N], vis_[N];
int w[N], c[N];
struct Edge {
int from, to, dis, c, id, net;
} a[N << 1], e[N << 1];
struct ANS {
int id, dis;
} ans[N][30];
int Head[N], numa;
void add(int from, int to, int dis, int c, int id) {
a[++numa] = (Edge){from, to, dis, c, id, Head[from]};
Head[from] = numa;
}
int head[N], nume;
void add_edge(int from, int to, int dis, int c, int id) {
e[++nume] = (Edge){from, to, dis, c, id, head[from]};
head[from] = nume;
}
int fath[N][30], dep[N];
void dfs(int x, int fa) {
fath[x][0] = fa;
// cout << x << " " << fa << endl;
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (to == fa)
continue;
dep[to] = dep[x] + 1ll;
ans[to][0].dis = e[i].dis;
ans[to][0].id = e[i].id;
dfs(to, x);
}
}
void pre() {
for (int i = 1; i <= 26; i++) {
for (int j = 1; j <= n; j++) {
// cout<< fath[j][0]<<endl;
fath[j][i] = fath[fath[j][i - 1]][i - 1];
if (ans[j][i - 1].dis > ans[fath[j][i - 1]][i - 1].dis) {
ans[j][i].dis = ans[j][i - 1].dis;
ans[j][i].id = ans[j][i - 1].id;
} else {
ans[j][i].dis = ans[fath[j][i - 1]][i - 1].dis;
ans[j][i].id = ans[fath[j][i - 1]][i - 1].id;
}
}
}
}
int LCA(int x, int y) {
if (dep[x] > dep[y])
swap(x, y);
for (int i = 26; i >= 0; --i) {
if (dep[x] > dep[fath[y][i]])
continue;
y = fath[y][i];
}
if (x == y)
return x;
for (int i = 26; i >= 0; i--) {
if (fath[x][i] != fath[y][i])
x = fath[x][i], y = fath[y][i];
}
return fath[x][0];
}
ANS q_max(int x, int y) {
ANS Ans;
Ans.dis = -1e9 - 100;
for (int i = 26; i >= 0; --i) {
if (dep[fath[x][i]] >= dep[y]) {
if (Ans.dis < ans[x][i].dis) {
Ans.dis = ans[x][i].dis;
Ans.id = ans[x][i].id;
}
x = fath[x][i];
}
}
return Ans;
}
int f[N];
int findf(int x) {
return f[x] == x ? x : f[x] = findf(f[x]);
}
bool cmp(Edge a, Edge b) {
return a.dis < b.dis;
}
void kur() {
for (int i = 1; i <= n; i++)
f[i] = i;
sort(a + 1, a + m + 1, cmp);
for (int i = 1; i <= m; i++) {
int x = findf(a[i].from), y = findf(a[i].to);
if (x == y)
continue;
add_edge(a[i].from, a[i].to, a[i].dis, a[i].c, i);
add_edge(a[i].to, a[i].from, a[i].dis, a[i].c, i);
f[x] = y;
val += a[i].dis;
vis[i] = true;
vis_[i] = true;
}
}
int temp = 1e18 + 100;
void END() {
for (int i = 1; i <= m; i++) {
int val_ = s / a[i].c;
if (vis[i]) {
if (temp > val - val_) {
flag = i;
}
temp = min(temp, val - val_);
} else {
int lca = LCA(a[i].from, a[i].to);
// cout << a[i].from << " " << a[i].to << endl;
// cout << lca << ' ';
ANS maxu = q_max(a[i].from, lca);
ANS maxv = q_max(a[i].to, lca);
if (maxu.dis > maxv.dis) {
if (temp > val - maxu.dis + a[i].dis - val_) {
temp = val - maxu.dis + a[i].dis - val_;
vis_[pre_jia] = false;
vis_[pre_jian] = true;
vis_[i] = true;
vis_[maxu.id] = false;
pre_jia = i;
pre_jian = maxu.id;
flag = i;
}
} else {
if (temp > val - maxv.dis + a[i].dis - val_) {
temp = val - maxv.dis + a[i].dis - val_;
vis_[pre_jia] = false;
vis_[pre_jian] = true;
vis_[i] = true;
vis_[maxv.id] = false;
pre_jia = i;
pre_jian = maxv.id;
flag = i;
}
}
}
}
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= m; i++)
w[i] = read();
for (int i = 1; i <= m; i++)
c[i] = read();
for (int i = 1, u, v; i <= m; i++) {
u = read(), v = read();
add(u, v, w[i], c[i], i);
}
s = read();
kur();
dep[1] = 1;
ans[1][0].dis = e[1].dis;
ans[1][0].id = e[1].id;
dfs(1, 1);
pre();
END();
if (flag) {
a[flag].dis = a[flag].dis - (s / a[flag].c);
}
printf("%lld\n", temp);
for (int i = 1; i <= m; i++) {
if (vis_[i]) {
// cout << i<< ' ';
printf("%lld %lld\n", a[i].id, a[i].dis);
}
}
// system("pause");
return 0;
}
强连通分量
2- SAT
- 两个点 \(x_i \& x_j = false\)
- \(\rightarrow add(false(x_i),x_j) ,add(false(x_j), x_i)\)
- 然后跑强连通分量即可,如果\(x 和 false(x)\)在同一个强连通分量中,就无解
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <string>
#define ll long long
using namespace std;
const int N = 5e5 + 100;
const int inf = 1e9;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
struct Edge {
int from, to, net;
} e[N << 1];
int head[N], nume;
void add_edge(int from, int to) {
e[++nume] = (Edge){from, to, head[from]};
head[from] = nume;
}
int st[N], sn, vis[N];
int dfn[N], low[N], cnt;
int scc, rd[N], cd[N], sum[N];
int num[N];
int n, m, s, p;
int start;
void tir(int x) {
st[++sn] = x, vis[x] = 1;
low[x] = dfn[x] = ++cnt;
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (!dfn[to])
tir(to), low[x] = min(low[x], low[to]);
else if (vis[to])
low[x] = min(low[x], dfn[to]);
}
if (dfn[x] == low[x]) {
int top = st[sn--];
vis[top] = 0; scc++;
num[top] = scc;
while (top != x) {
top = st[sn--];
vis[top] = 0;
num[top] = scc;
}
}
}
int fr(int x) {
return (x & 1) ? (x + 1) : (x - 1);
}
int main() {
n = read(), m = read();
for (int i = 1; i <= m; i++) {
int u = read(), v = read();
add_edge(u, fr(v));
add_edge(v, fr(u));
}
for (int i = 1; i <= 2 * n; i++) {
if (!dfn[i])
tir(i);
}
for (int i = 1; i <= 2 * n; i++) {
if ((i & 1) && num[i] == num[i + 1]) {
puts("NIE");
system("pause");
return 0;
}
}
for (int i = 1; i <= 2 * n; i++) {
if (num[i] < num[fr(i)])
printf("%d\n", i);
}
system("pause");
return 0;
}
虽然写着模板但是确实比上面那个题难一点,建图的时候要弄清楚逻辑关系
题目要求的是两者中满足一个即可
所以:
每一建边的时候我们\(false \to true\) 建边即可
我们令哪一个条件取反,就用这个相反的条件向未更改的条件连边,因为这里的\(false 和true\)是相对而言的,并非是单纯的\(0 \to 1\)建边而是条件不满足的向条件满足的建边
举个小样例:
要求满足的是\(a = 0 , b = 0\),那么我们就对其中一个取反\(\lnot a \to b\)和\(\lnot b \to a\)都建一条边以此类推即可
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
#include <string>
#define ll long long
using namespace std;
const int N = 2e6 + 100;
const int inf = 1e9;
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
struct Edge {
int from, to, net;
} e[N << 1];
int head[N], nume;
void add_edge(int from, int to) {
e[++nume] = (Edge){from, to, head[from]};
head[from] = nume;
}
int st[N], sn, vis[N];
int dfn[N], low[N], cnt;
int scc, rd[N], cd[N], sum[N];
int num[N];
int n, m, s, p;
int start;
void tir(int x) {
st[++sn] = x, vis[x] = 1;
low[x] = dfn[x] = ++cnt;
for (int i = head[x]; i; i = e[i].net) {
int to = e[i].to;
if (!dfn[to])
tir(to), low[x] = min(low[x], low[to]);
else if (vis[to])
low[x] = min(low[x], dfn[to]);
}
if (dfn[x] == low[x]) {
int top = st[sn--];
vis[top] = 0;
scc++;
num[top] = scc;
while (top != x) {
top = st[sn--];
vis[top] = 0;
num[top] = scc;
}
}
}
int main() {
n = read(), m = read();
for (int i = 1; i <= m; i++) {
int x = read(), a = read(), y = read(), b = read();
if (a && b)
add_edge(x + n, y), add_edge(y + n, x);
if (!a && b)
add_edge(x, y), add_edge(y + n, x + n);
if (a && !b)
add_edge(x + n, y + n), add_edge(y, x);
if (!a && !b)
add_edge(x, y + n), add_edge(y, x + n);
}
for (int i = 1; i <= 2 * n; i++) {
if (!dfn[i])
tir(i);
if (i <= n && num[i] == num[i + n]) {
puts("IMPOSSIBLE");
system("pause");
return 0;
}
}
puts("POSSIBLE");
for (int i = 1; i <= n; i++) {
printf("%d ", num[i] < num[i + n]);
}
system("pause");
return 0;
}
无向图中的联通分量
\(\begin{cases} 边双联通 \to 缩点后变成一棵树\\ 点双联通\\ \end{cases}\)
边双:找边双等价于找桥
void tir(int x ,int fa) {
low[x] = dfn[x] ++cnt;
for(int i = head[x] ; i ; i = e[i].net) {
int to = e[i].to;
if(!dfn[to]) {
tir(to, x)
low[x] = min(low[x], low[to]);
if(low[to] > dfn[x]) cut[x] = 1;
} else
if(dfn[to] < dfn[x] && to != fa)
low[x] = min(low[x],dfn[to]);
}
}
二分图
匈牙利算法
- A:A国中最短多选择两个人
- B:B国中可以根据奇偶性分两类
upd 2021.4.14
我怕是属鸽子的自己咋这么多东西都没写,也不想补就这样放出来吧,复习的时候再补补
至于二分图,大概我是不会写的,我可能会写网络流? 因为二分图想想实现的东西网络流都能够实现