图论算法
说明
弥补一下图论方面的欠缺
二维图上的并查集
#include <iostream>
#include <cstring>
using std::cout;
struct Dsu {
const int size;
int f[1000];
int numOfBlock[1000];
int numOfChest[1000];
Dsu(int size)
:size(size)
{
memset(f,-1,sizeof(f));
memset(numOfBlock,0,sizeof(numOfBlock));
memset(numOfChest,0,sizeof(numOfChest));
}
int find(int a,int b) {
return find(id(a,b));
}
int find(int n) {
if (f[n] == -1)
return n;
else
return f[n] = find(f[n]);
}
int id(int a,int b) {
return a * size + b;
}
bool is_same(int a,int b) {
return find(a) == find(b);
}
void uni(int a,int b,int c,int d) {
uni(id(a,b),id(c,d));
}
void mergeInfo(int a,int b) {
numOfChest[find(b)] += numOfChest[find(a)];
numOfBlock[find(b)] += numOfBlock[find(a)];
numOfChest[find(a)] = 0;
numOfBlock[find(a)] = 0;
}
void uni(int a,int b) {
if (!is_same(a,b)) {
mergeInfo(a,b);
f[find(a)] = find(b);
}
}
};
char g[100][100] = {
" 1 ",
" 21 ",
"1111 11111",
" 1 ",
" 12 ",
" 1 ",
" 2 1 ",
" 1 ",
" 2 1 ",
" 1 ",
};
void checkAndUni(Dsu & dsu,int a,int b,int c,int d) {
if (a < 0 or b < 0 or c < 0 or d < 0) {
return;
}
else if (a >= dsu.size or b >= dsu.size or c >= dsu.size or d >= dsu.size) {
return;
}
else if (g[a][b] == '1' or g[c][d] == '1'){
return;
}
else {
dsu.uni(a,b,c,d);
}
}
int main() {
const int size = 10;
Dsu dsu(10);
for (int i = 0;i < size;++i) {
for (int k = 0;k < size;++k) {
if (g[i][k] != '1') {
auto id = dsu.id(i,k);
if (g[i][k] == '2') {
dsu.numOfChest[id] = 1;
}
else {
dsu.numOfBlock[id] = 1;
}
}
}
}
for (int i = 0;i < size;++i) {
for (int k = 0;k < size;++k) {
if (g[i][k] != '1') {
checkAndUni(dsu,i,k,i - 1,k);
checkAndUni(dsu,i,k,i + 1,k);
checkAndUni(dsu,i,k,i,k - 1);
checkAndUni(dsu,i,k,i,k + 1);
}
}
}
for (int i = 0;i < size;++i) {
for (int k = 0;k < size;++k) {
cout << dsu.find(i,k) << " ";
}
cout << "\n";
}
int a = 1,b = 1;
auto id = dsu.find(dsu.id(a,b));
cout << "(" << a << "," << b << ")" << " = " << dsu.numOfBlock[id] << " " << dsu.numOfChest[id] << "\n";
return 0;
}
基环树
就是树 + 一个边,
有向的分为内向树和外向树
找环 : 拓扑排序、tarjan、一个简化版本的dfs
一般就是把环和非环拆开看,或者把一个边断开,强行维护
最大流
最大流最小割定理
下列条件等价
f是G的一个最大流
残留网络\(G_f\)不包含增广路径
对G的某个割(S,T)有,\(f = c(S,T)\)
基本思路
把图等效成单源单汇的图
最大流 = 最小割,最小割就是割掉若干条边
把图分成分别包含源和汇的两部分,的边的流量和
思路非常简单,首先对于每条边,增加一条流量为0的反向边
然后不断的找增广路,对于每条找到的增光路,增加反向边
然后不停按照这个思路贪心就行了
不带任何优化的算法 : Edmonds_Karp
\(O(VE^2)\)
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
namespace Forward_Star
{
struct node
{
ll to,net,val;
node(ll to = 0,ll net = 0,ll val = 0)
:to(to),net(net),val(val)
{}
};
vector<node> edge(2);
vector<ll> head;
void init(ll n,ll m)
{
edge.resize(2);
head = vector<ll>(n + 1);
}
void add_edge(ll a,ll b,ll c)
{
edge.emplace_back(b,head[a],c);
head[a] = edge.size() - 1;
}
}
namespace Edmonds_Karp
{
vector<ll> pre;
void init(ll n,ll m)
{
pre.resize(n + 1);
}
ll bfs(ll n,ll star,ll fini)
{
vector<ll> vis(n + 1),dis(n + 1);
queue<ll> q;
q.push(star);
vis[star] = true;
dis[star] = numeric_limits<ll>::max();
while(!q.empty())
{
auto x = q.front();q.pop();
for(ll i = Forward_Star::head[x];i;i = Forward_Star::edge[i].net)
{
auto it = Forward_Star::edge[i].to;
if (Forward_Star::edge[i].val == 0)
continue; //我们只关心剩余流量>0的边
if (vis[it] == 1)
continue; //这一条增广路没有访问过
dis[it] = min(dis[x],Forward_Star::edge[i].val);
pre[it] = i; //记录前驱,方便修改边权
q.push(it);
vis[it] = 1;
if (it == fini)
return dis[fini]; //找到了一条增广路
}
}
return 0;
}
void update(ll star,ll fini,ll add)
{
for (auto x = fini;x != star;)
{
auto it = pre[x];
Forward_Star::edge[it].val -= add;
Forward_Star::edge[it ^ 1].val += add;
x = Forward_Star::edge[it ^ 1].to;
}
}
}
int main()
{
ll n,m,star,fini;
cin >> n >> m >> star >> fini;
Forward_Star::init(n,m);
Edmonds_Karp::init(n,m);
for (ll i = 1;i <= m;i++)
{
ll a,b,c;
cin >> a >> b >> c;
Forward_Star::add_edge(a,b,c);
Forward_Star::add_edge(b,a,0);
}
ll ret = 0;
for (auto add = Edmonds_Karp::bfs(n,star,fini);
add;
add = Edmonds_Karp::bfs(n,star,fini)
)
{
ret += add;
Edmonds_Karp::update(star,fini,add);
}
cout << ret << "\n";
return 0;
}
压入与重标记算法
重标记与前移算法
二分图匹配
完全匹配
指匹配数为\(min(n,m)\)的匹配
hall定理---用来判断二分图是不是有完全匹配
设n为左部,m为右部,且n < m,如果对于任意k from 1 to n,都有
左部任意k个点的邻居不少于k个
那么该二分图完美匹配
Hopcroft-Karp算法
复杂度\(O(m\sqrt{n})\)
该算法主要是对匈牙利算法的优化,在寻找增广路径的时候同时寻找多条不相交的增广路径,形成极大增广路径集,然后对极大增广路径集进行增广。在寻找增广路径集的每个阶段,找到的增广路径集都具有相同的长度,且随着算法的进行,增广路径的长度不断的扩大。可以证明,最多增广n^0.5次就可以得到最大匹配。
KM算法
复杂度O(VE)
非常好理解,能增广就增广
贪心可以由hall定理证明正确性
最后x_match存的就是x的匹配点
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
namespace Km
{
vector<vector<ll>> g;
void init(ll n,ll m)
{
g = vector<vector<ll>>(n + 1);
}
void add_edge(ll a,ll b)
{
g[a].emplace_back(b);
}
bool dfs(ll now,vector<ll> & x_match,vector<ll> & y_match,vector<ll> & vis)
{
for (auto it : g[now])
if (vis[it] == 0)
{
vis[it] = 1;
if (y_match[it] == -1 or dfs(y_match[it],x_match,y_match,vis))
{
x_match[now] = it;
y_match[it] = now;
return 1;
}
}
return 0;
}
ll maxmatch(ll n,ll m)
{
ll ret = 0;
vector<ll> x_match(n + 1,-1),y_match(m + 1,-1);
//for (ll i = n;i >= 1;i--)
for (ll i = 1;i <= n;i++)
{
vector<ll> vis(m + 1);
ret += dfs(i,x_match,y_match,vis);
}
return ret;
}
}
int main()
{
ll n,m,e;
cin >> n >> m >> e;
Km::init(n,m);
for (ll i = 0;i < e;i++)
{
ll x,y;
cin >> x >> y;
if (x < 1 or x > n or y < 1 or y > m)
continue;
Km::add_edge(x,y);
}
cout << Km::maxmatch(n,m);
}
生成树
DSU
值得一提的是make_root,可以把一个元素变成代表元
namespace Dsu
{
vector<ll> f;
void init(ll n)
{
f.resize(n + 1);
}
ll find(ll n)
{
if (f[n] == 0)
return n;
else
return f[n] = find(f[n]);
}
bool is_same(ll a,ll b)
{
return find(a) == find(b);
}
void uni(ll a,ll b)
{
if (find(a) != find(b))
f[find(a)] = find(b);
}
void make_root(ll now)
{
find(now);
if (f[now] != 0)
{
ll fa = f[now];
f[fa] = now;
f[now] = 0;
}
}
}
克鲁斯卡尔
复杂度\(O(V + Elog(E))\)
prim算法
复杂度\(O((V + E)logV)\)
值得注意的是和迪杰斯特拉算法相比
迪杰斯特拉 : priorirty_queue.push(min_distance[now] + edge.cost)
prim : priorirty_queue.push(edge.cost)
最短路
定义:
V:图中的点数
E:图中的边数
链式前向星 \(O(V + E)\)
常数小
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
namespace Forward_Star
{
vector<tuple<ll,ll,ll>> edge; //(to,next,weight)
vector<ll> head;
void init(ll n)
{
head = vector<ll>(n + 1,-1);
}
void add_edge(ll a,ll b,ll c)
{
edge.emplace_back(b,head[a],a);
head[a] = edge.size() - 1;
}
void show(ll n)
{
for (ll i = 1;i <= n;i++)
{
for (ll k = head[i];k != -1;k = get<1>(edge[k]))
{
ll from = i;
ll to,weight;
std::tie(to,std::ignore,weight) = edge[k];
cout << "debug+ " << from << " " << to << " " << weight << "\n";
}
}
}
}
int main()
{
ll n,m;
cin >> n >> m;
Forward_Star::init(n);
for (ll i = 1;i <= m;i++)
{
ll a,b,c;
cin >> a >> b >> c;
Forward_Star::add_edge(a,b,c);
}
Forward_Star::show(n);
return 0;
}
/*
Test data :
5 7
1 2 1
2 3 2
3 4 3
1 3 4
4 1 5
4 5 6
4 5 7
*/
floyd \(O(V^3)\)
/*
---- From XDU's mzb
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
namespace Floyd
{
vector<tuple<ll,ll,ll>> edge;
void clear()
{
edge.clear();
}
void add_edge(ll a,ll b,ll c)
{
edge.emplace_back(a,b,c);
}
vector<vector<ll>> run(ll n)
{
vector<vector<ll>> ret(n + 1,vector<ll>(n + 1,numeric_limits<ll>::max()));
for (ll i = 1;i <= n;i++)
ret[i][i] = 0;
for (auto it : edge)
{
ll a,b,c;
tie(a,b,c) = it;
ret[a][b] = min(ret[a][b],c);
}
for (ll k = 1;k <= n;k++) // 枚举中转点
for (ll i = 1;i <= n;i++)
for (ll j = 1;j <= n;j++)
{
if (ret[i][k] != numeric_limits<ll>::max() and ret[k][j] != numeric_limits<ll>::max())
{
ret[i][j] = min(ret[i][j],ret[i][k] + ret[k][j]);
}
}
return ret;
}
}
int main()
{
ll n,m,star;
cin >> n >> m >> star;
for (ll i = 1;i <= m;i++)
{
ll a,b,c;
cin >> a >> b >> c;
Floyd::add_edge(a,b,c);
}
auto fin = Floyd::run(n);
auto ans = fin[star];
for (ll i = 1;i < ll(ans.size());i++)
cout << ans[i] << (i + 1 == ll(ans.size()) ? "" : " ");
return 0;
}
dijkstra \(O((V + E)log_2{E})\)
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
namespace Dijkstra
{
vector<pair<ll,ll>> g[100000 + 110];
void clear(ll n)
{
for (ll i = 1;i <= n;i++)
g[i].clear();
}
void add_edge(ll a,ll b,ll c)
{
g[a].emplace_back(b,c);
}
vector<ll> run(ll n,ll star)
{
vector<ll> min_distance(n + 1,numeric_limits<ll>::max());
priority_queue<pair<ll,ll>,vector<pair<ll,ll>>,greater<pair<ll,ll>>> min_queue;
min_queue.emplace(0,star);
while (!min_queue.empty())
{
auto now = min_queue.top();min_queue.pop();
if (min_distance[now.second] != numeric_limits<ll>::max())
continue;
else
min_distance[now.second] = now.first;
for (auto it : g[now.second])
if (min_distance[it.first] == numeric_limits<ll>::max())
min_queue.emplace(it.second + min_distance[now.second],it.first);
}
return min_distance;
}
}
int main()
{
ll n,m,star;
cin >> n >> m >> star;
for (ll i = 1;i <= m;i++)
{
ll a,b,c;
cin >> a >> b >> c;
Dijkstra::add_edge(a,b,c);
}
auto ans = Dijkstra::run(n,star);
for (ll i = 1;i < ll(ans.size());i++)
cout << ans[i] << (i + 1 == ll(ans.size()) ? "": " ");
return 0;
}
bellman-ford \(O(VE)\)
增广V轮后还能增广,就是存在负权回路
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
namespace Bellman_Ford
{
vector<tuple<ll,ll,ll>> edge;
void clear(ll n)
{
edge.clear();
}
void add_edge(ll a,ll b,ll c)
{
edge.emplace_back(a,b,c);
}
bool relax(vector<ll> & min_distance,ll a,ll b,ll c)
{
if (min_distance[a] != numeric_limits<ll>::max())
{
if (min_distance[a] + c < min_distance[b])
{
min_distance[b] = min_distance[a] + c;
return 1;
}
}
return 0;
}
vector<ll> run(ll n,ll star)
{
vector<ll> min_distance(n + 1,numeric_limits<ll>::max());
min_distance[star] = 0;
while (1)
{
bool is_relaxed = 0;
for (auto it : edge)
{
ll a,b,c;
tie(a,b,c) = it;
is_relaxed |= relax(min_distance,a,b,c);
}
if (!is_relaxed)
break;
}
return min_distance;
}
}
int main()
{
ll n,m,star;
cin >> n >> m >> star;
for (ll i = 1;i <= m;i++)
{
ll a,b,c;
cin >> a >> b >> c;
Bellman_Ford::add_edge(a,b,c);
}
auto ans = Bellman_Ford::run(n,star);
for (ll i = 1;i < ll(ans.size());i++)
cout << ans[i] << (i + 1 == ll(ans.size()) ? "": " ");
return 0;
}
SPFA
期望\(O(kE)\),其中k是一个常数
最坏\(O(VE)\)
Bellman_Ford的一个队列优化,思路没什么创新
咕咕咕
Johnson全源最短路
稠密图上弗洛伊德算法最快
先来一次BellmanFord算法,把图变成正权图,然后来n次Dijkstra,复杂度\(O(V^2logV + VE)\)
单纯给所有边加正权是没有用的,没有改变负权图的根本性质
edge old,new;
ll star = n + 1;
for (ll i = 1;i <= n;i++)
{
old.add_edge(star,i,0);
}
auto min_distance = Bellman_Ford(old,n + 1,star);
for (auto it : edge)
{
tie(a,b,c) = it;
new.add_edge(a,b,c + min_distance[a] - min_distance[b]);
}
for (ll i = 1;i <= n;i++)
Dijkstra(new,i);
证明
当确定源点,跑一次最短路后,min_distance就拥有类似势能的性质
也就是(min_distance[a] - min_distance[b]) - (min_distance[b] - min_distance[c]) = min_distance[a] - min_distance[c]
最短路性:
非负性:有三角不等式,可简单证明新图的边权都非负
差分约束
解由若干\(x_i - x_j \leq b_k\)组成的方程
建一个超级源点 n + 1 并向所有点连权值为 0 的边是为了求一组非正数的可行解
for (ll i = 1;i <= n;i++)
{
Bellman_Ford::add_edge(n + 1,i,0);
}
for (ll i = 1;i <= m;i++)
{
ll x_i,x_j,b_k;
cin >> x_i >> x_j >> b_k;
Bellman_Ford::add_edge(x_j,x_i,b_k);
}
bool has_ans = Bellman_Ford::run(n + 1,n + 1);
然后从超级源(n + 1)开始跑最短路就可以
有负权回路就无解,反之有解,解为
\(\{x_1,x_2...x_n\} = \{ min\_distance(源,v_1),min\_distance(源,v_2)...min\_distance(源,v_n)\}\)
相较于其他解,这个解满足\(\sum x_i\)最大同时 \((\max x_i - \min x_i)\)最小
注意我们在连源和其他点的时候,意味着加了约束\(x_i \leq 0\)
拓扑排序
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
namespace Topological_Sorting
{
vector<vector<ll>> g;
void init(ll n)
{
g = vector<vector<ll>>(n + 1);
}
void add_edge(ll a,ll b)
{
g[a].emplace_back(b);
}
vector<ll> run(ll n)
{
vector<ll> degree(n + 1);
for (ll i = 1;i <= n;i++)
{
for (auto it : g[i])
degree[it]++;
}
queue<ll> que;
for (ll i = 1;i <= n;i++)
if (degree[i] == 0)
que.emplace(i);
vector<ll> ret;
while (que.size())
{
ll now = que.front();que.pop();
for (auto it : g[now])
{
degree[it]--;
if (degree[it] == 0)
que.emplace(it);
}
ret.emplace_back(now);
}
return ll(ret.size()) == n ? ret : vector<ll>();
}
}
int main()
{
ll n,m;
cin >> n >> m;
Topological_Sorting::init(n);
for (ll i = 1;i <= m;i++)
{
ll a,b;
cin >> a >> b;
Topological_Sorting::add_edge(a,b);
}
auto ret = Topological_Sorting::run(n);
for (auto it : ret)
cout << it << " ";
return 0;
}
连通性问题
dfs树
dfs树里面只有两种边
父子边 : 黑边
返祖边 : 绿边
下面两种边不出现
横向边 : 红边
正向边 : 也就是调到dfs树子孙的非父子边
dfs的时候,
记\(d[v]\)为,当顶点v被第一次发现时,的时间戳 (发现时间)
记\(f[v]\)为,当顶点v的所有相邻点都被dfs后,的时间戳 (结束时间)
Tarjan 强连通分量
相当于给点分了组,组内强联通
dfn就是时间序
low就是该点能走到的,时间序最小的节点,的时间序
那么很显然,当\(low[now] = dfn[now]\)的时候,now就是dfs树上强连通分量的根
把所有被dfn[now]标记的点退栈即可
namespace Tarjan
{
vector<vector<ll>> g;
vector<ll> low,dfn,vis;
std::stack<ll> stack;
void init(ll n)
{
g = vector<vector<ll>>(n + 1);
low = dfn = vis = vector<ll>(n + 1);
stack = std::stack<ll>();
}
void add_edge(ll a,ll b)
{
g[a].emplace_back(b);
}
void run(ll now,vector<ll> & id,ll & time)
{
low[now] = dfn[now] = ++time;
stack.emplace(now);
vis[now] = 1;
for (auto it : g[now])
{
if (!dfn[it])
{
run(it,id,time);
low[now] = min(low[now],low[it]);
}
else if (vis[it])
{
low[now] = min(low[now],low[it]);
}
}
if (dfn[now] == low[now])
{
while (stack.size() and stack.top())
{
ll t = stack.top();
id[t] = now;
vis[t] = 0;
stack.pop();
if (now == t)
break;
}
}
}
vector<ll> run(ll n)
{
vector<ll> ret(n + 1);
ll time = 0;
for (ll i = 1;i <= n;i++)
if (!dfn[i])
run(i,ret,time);
return ret;
}
}
tarjin求割点
#include <iostream>
#include <vector>
#include <cstring>
using std::vector;
using std::string;
using std::cout;
using std::cin;
using std::min;
struct CutPoint {
static const int maxn = 200000 + 10;
int dfn[maxn];
int low[maxn];
int cut[maxn];
int dfs_clock;
vector<int> e[maxn];
int n;
CutPoint(int n)
:n(n),dfs_clock(0)
{
memset(cut,0,sizeof(cut));
}
void addEdge(int a,int b) {
e[a].emplace_back(b);
e[b].emplace_back(a);
}
vector<int> getCutPoint() {
dfs_clock = 0;
for(int i = 1;i <= n;i++) {
// 假设不是连通图 ,连通图跑一次就行了
if (!dfn[i]) {
tarjan(i,i,-1);
}
}
vector<int> result;
for (int i = 1;i <= n;++i) {
if (cut[i]) {
result.push_back(i);
}
}
return result;
}
void tarjan(int now,int root,int fa) {
//记录当前节点、树的根节点、父节点
dfn[now] = low[now] = ++dfs_clock;
//初始时,low[now] = dfn[now]
int child = 0;//记录该节点的孩子数
for (auto to : e[now]) {
if(!dfn[to]) {
// 若 to 尚未访问过
child++;//to是now的孩子
tarjan(to,root,now);//向下遍历
low[now] = min(low[now],low[to]);//更新now
if (low[to] >= dfn[now] and now != root) {
cut[now] = 1;
}
}
else if (to != fa) {
low[now] = min(low[now],dfn[to]);
}
//若to已访问过,且to不是父节点,则to是祖先节点
}
if (child >= 2 and now == root) {
cut[now] = 1;
}
}
};
int main() {
int n,m;
cin >> n >> m;
CutPoint cut(n);
for (int i = 1;i <= m;++i) {
int a,b;
cin >> a >> b;
cut.addEdge(a,b);
}
auto result = cut.getCutPoint();
cout << result.size() << "\n";
for (auto it : result) {
cout << it << " ";
}
return 0;
}
本文来自博客园,作者:XDU18清欢,转载请注明原文链接:https://www.cnblogs.com/XDU-mzb/p/15393460.html