目录

  • 一、总述
  • 二、常见的有针对性的算法
    • - 针对点权
    • - 针对边权
  • 三、拆点
    • - 过程
    • - 实例
    • - 网络流
  • 四、拆边
    • - 过程
    • - 实例
    • - 倍增算法(Kruskal 重构树)
    • - LCT 维护最小生成树
  • 五、总结

一、总述

在图论中,一张图由点和边构成。而点和边都可以作为信息的载体,比如说点权和边权。尽管点和边看似如此接近,但是它们的性质确实截然不同的。点表示的是一种实质上的状态,而边表示的是一种虚拟的状态间的转移。

因此,有一些图论算法只能处理点上的信息,而另一些图论算法只能处理边上的信息。怎样使得这些针对性的算法通用化呢?某些情况下,我们可以通过拆点和拆边的方式来解决。

二、常见的有针对性的算法

- 针对点权

树链剖分(套线段树或树状数组)

  • Link-Cut Tree
  • 倍增
  • 强连通分量缩点

- 针对边权

最短路

  • 最小生成树
  • 网络流
  • 匈牙利算法
  • 拓扑排序

容易看出,数据结构型的算法一般针对点权,因为维护的是实体上的数据;而图论算法一般容易维护边权,因为在点与点之间通过边转移时,容易将边权一起转移走。

由于无向边可以当做两条有向边,因此下文均以有向边介绍。

三、拆点

- 过程

对于某个点权为 w 的点 v,我们可以把 v 点拆成 v1 和 v2 两个点,其中 v1 称为入点,v2 称为出点。从 v1→v2 连一条权值为 ​w 的有向边。此外对于图上原本连接某两点 x 和 y,权值为 z 的有向边,改为从 x2→y1 连权值为 z 的边。这就是拆点的主要过程。

上图说明了拆点的过程。

- 实例

带点权和边权的最短路
其实这个不用拆点也能做,就用这个来作为拆点的入门好了。

有一张 n 个点,m 条边的有向图,点有点权,边有边权。定义一条路径的长度为这条路径经过的所有点的点权和加上经过的所有边的边权和。求 1
号点到 n 号点的最短路。

将点拆成入点和出点,从入点向出点连权值为该点点权的边。直接跑一遍起点为 1 号点的入点,终点为 n 号点的出点的单源最短路即可。

void solve() {cin >> N >> M;for (int i = 1; i <= N; ++i) {int x; cin >> x; // 输入 i 点点权add_edge(i, i + N, x);}for (int i = 1; i <= M; ++i) {int u, v, w; // 一条从 u 到 v 权值为 w 的单向边cin >> u >> v >> w;add_edge(u + N, v, w);}Dijkstra(1); // 做一次源点为 1 的单源最短路径printf("%d\n", dis[N + N]); // N 号点的出点的距离即为答案
}

- 网络流

网络流上的流量都在边上,因此网络流属于针对边权的典型图论算法。当点上有权值时,都以拆点的形式解决。

直接举一道例题 方格取数加强版

给出一个 n×n 的矩阵,每一格有一个非负整数 Ai,j (Ai,j≤1000)。现在从 (1,1) 出发,可以往右或者往下走,最后到达
(n,n)。每达到一格,把该格子的数取出来,该格子的数就变成 0,这样一共走 K 次,现在要求 K 次所达到的方格的数的和最大。

很明显,这里的权值在点上。考虑拆点,将每个点拆成入点和出点。

由于每个格子的数只能取一次,因此我们从入点向出点连一条流量为 1,权值为 Ai,j 的边。由于可以无数次经过,再从入点向出点连流量为 ∞,权值为 0 的边。

同时为了转移,我们从一个格子的出点向其右方、下方的格子的入点连流量为 ∞,权值为 0 的边。

最后求解原图上 (1,1) 的入点到 (n,n) 的出点的流量为 K​ 的最小费用流即可。
拆点技巧:

int num(int i,int j,int k){return (i - 1) * n + j + k * n * n;
}

具体讲解:
C++学习笔记:图论——拆点详解

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<math.h>
#include<cstring>
#include<queue>
//#define ls (p<<1)
//#define rs (p<<1|1)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
//#define lowbit(p) p&(-p)
using namespace std;typedef long long ll;
typedef pair<int,int> PII;
const ll INF = 1e18;
const int N = 5e3+7;
const int M = 5e5+7;
int maxflow,s,t,k;
int n,m,ans,e;
int head[N],ver[M],nex[M],edge[M],cost[M],tot;
bool vis[N];
int dis[N],incf[N],pre[N];void add(int x,int y,int z,int c){//正边反边ver[++tot] = y;edge[tot] = z;cost[tot] = c;nex[tot] = head[x];head[x] = tot;ver[++tot] = x;edge[tot] = 0;cost[tot] = -c;nex[tot] = head[y];head[y] = tot;
}int num(int i,int j,int k){return (i - 1) * n + j + k * n * n;
}bool spfa(){//spfa求最长路queue<int>q;memset(vis,0,sizeof vis);memset(dis,0xcf,sizeof dis);//-INFq.push(s);dis[s] = 0;vis[s] = 1;incf[s] = 1<<30;//增广路各边的最小剩余容量while(q.size()){int x = q.front();q.pop();vis[x] = 0;//spfa的操作for(int i = head[x];i;i = nex[i]){if(edge[i]){//剩余容量要>0,才在残余网络中int y = ver[i];if(dis[y] < dis[x] + cost[i]){dis[y] = dis[x] + cost[i];incf[y] = min(incf[x],edge[i]);//最小剩余容量pre[y] = i;//记录前驱(前向星编号),方便找到最长路的实际方案if(!vis[y])vis[y] = 1,q.push(y);}}}}if(dis[t] == 0xcfcfcfcf)return false;//汇点不可达,已求出最大流return true;
}//EK的老操作了,更新最长增广路及其反向边的剩余容量
void update(){int x = t;while(x != s){int i = pre[x];edge[i] -= incf[t];edge[i ^ 1] += incf[t];//成对变换,反边加x = ver[i ^ 1];//反边回去的地方就是上一个结点}maxflow += incf[t];//顺便求最大流ans += dis[t] * incf[t];//题目要求
}void EK(){while(spfa())//疯狂找增广路update();
}int main(){cin>>n>>k;s = 1;t = 2 * n * n;tot = 1;over(i,1,n)over(j,1,n){int c;scanf("%d",&c);add(num(i,j,0),num(i,j,1),1,c);//自己(入点0)与自己(出点1)add(num(i,j,0),num(i,j,1),k-1,0);//两条边(取k次嘛,第一次有值,以后就没值了,用作下次选取)if(i < n)add(num(i,j,1),num(i+1,j,0),k,0);//自己(出点1)与下一行(入点0)或者下一列(入点0)if(j < n)add(num(i,j,1),num(i,j+1,0),k,0);}EK();printf("%d\n",ans);return 0;
}

四、拆边

当维护的是有根树的边权时,有一种更为方便的做法——权值下推。

我们可以让每个点维护其与其父亲的这条边的信息。即对于某个点 x,设其父亲节点为 f,从 f 到 x 的边权值为 w,那么我们可以直接让 x 点的点权加上 w。并且当更改边权 w 时,可以直接在点 x 的点权上修改。

特殊的是,当我们查询树上 u→v 路径信息时,我们需要减掉 lca(u,v) 的额外维护的边权,因为 lca(u,v) 维护的是在它上面的那条边的信息,不是我们需要的路径信息。

- 过程

对于一条连接 u, v ,权值为 w 的有向边 e,我们可以通过新建一个点 x,并将 x 的点权设为 w,从 u→x 和 v→x 各连一条有向边。这就是拆边的主要过程。


上图说明了拆边的过程。

通过拆边,我们就让维护点权的数据结构可以维护边权。

- 实例

- 倍增算法(Kruskal 重构树)

最小生成树是一种针对边权的算法,但是在一些生成树的题中,我们希望能够快速维护边权的信息。那么此时就可以在生成树时直接拆边,已达到我们的目的。这种最小生成树算法被称为 Kruskal 重构树。

Kruskal 重构树执行的过程与最小生成树的 Kruskal 算法类似:

  • 将原图的每一个节点看作一棵子树。
  • 合并两棵子树时,通过并查集找到它们子树对应的根节点,记作 u, v。
  • 新开一个节点 p,从 p 分别向 u, v 两点连边。于是 u 和 v 两棵子树就并到了一棵子树,根节点就是 p,并将 p 点权值赋为 u↔v 这条边的权值。

在代码上可以如此实现:

const int MaxN = 100000 + 5, MaxV = 200000 + 5;int N, M;
int cntv = N;  // 图中点数
int par[MaxV]; // 并查集(注意大小开为原图两倍)
int val[MaxV]; // 生成树中各点点权
struct edge { int u, v, w; } E[MaxM];
vector<int> Tree[MaxV];void Kruskal() {for (int i = 1; i <= N + N - 1; ++i) par[i] = i;sort(E + 1, E + 1 + M, cmp); // 按权值从小到大排序for (int i = 1; i <= N; ++i) val[i] = 0;for (int i = 1; i <= M; ++i) {int u = E[i].u, v = E[i].v;int p = Find(u), q = Find(v);if (p == q) continue;cntv++;par[p] = par[q] = cntv;val[cntv] = E[i].w;Tree[cntv].push_back(p);Tree[cntv].push_back(q);}
}

我们可以发现这样建出来的最小生成树(最大生成树同理)有如下性质:

  • 原最小生成树上 u 到 v 路径上的边权和就是现在 u 到 v 路径上的点权和。
  • 这是一个大根堆,也是一个二叉堆。
  • 原最小生成树上 u 到 v 路径上的最大值,就是 u, v 的最近公共祖先(LCA)的权值。故求最小瓶颈路时,可以使用 Kruskal 重构树的方法。

那么我们就看一道简单的例题:

[NOIP2013 提高组] 货车运输

给定一个 n 个点,m 条边的无向图,边上有权值。并有 q 次询问,每次询问输入两个点 u, v,找出一条路径,使得从 u 到 v
路径上的最小值最大,并输出这个最大的最小值;若从 u 不能到达 v,输出 −1.

使用 Kruskal 重构树算法建出最大生成树后,直接查询 u, v 两点的 LCA 权值即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;const int MAXN = 10000, MAXM = 50000, MAXQ = 30000;
const int MAXV = 20000, MAXE = 30000, MAXLOG = 20;int N, M, Q;
int U[MAXM+1], V[MAXM+1], W[MAXM+1];
int lnk[MAXM+1];
int X[MAXQ+1], Y[MAXQ+1], ans[MAXQ+1];
int Head[MAXV+1], To[MAXE+1], Next[MAXE+1];
int fa[MAXV+1][MAXLOG+1], val[MAXV+1], depth[MAXV+1]; // 树的信息
int par[MAXV+1], par2[MAXV+1];
int Head2[MAXV+1], To2[MAXQ*2+1], Next2[MAXQ*2+1], Num[MAXQ*2+1];
bool vis[MAXV+1];
int vs, es, qs;void init() {memset(fa, -1, sizeof fa );memset(depth, -1, sizeof depth );memset(val, 0x7F, sizeof val );scanf("%d %d", &N, &M);for (int i = 1; i <= N * 2; ++i) par[i] = i, par2[i] = i;for (int i = 1; i <= M; ++i) {lnk[i] = i;scanf("%d %d %d", &U[i], &V[i], &W[i]);}scanf("%d", &Q);for (int i = 1; i <= Q; ++i) scanf("%d %d", &X[i], &Y[i]);
}inline void add_edge(int from, int to) {es++;To[es] = to;Next[es] = Head[from];Head[from] = es;
}inline void add_query(int from, int to, int num) {qs++;To2[qs] = to;Num[qs] = num;Next2[qs] = Head2[from];Head2[from] = qs;
}inline bool cmp(int x, int y) { return W[x] > W[y]; }int Find(int x) { return par[x] == x ? x : par[x] = Find(par[x]); }
int Find2(int x) { return par2[x] == x ? x : par2[x] = Find2(par2[x]); }// Kruskal 重构树
void Kruskal() {sort(lnk + 1, lnk + 1 + M, cmp);vs = N;for (int I = 1; I <= M; ++I) {int i = lnk[I], u = U[i], v = V[i], w = W[i];int p = Find(u), q = Find(v);if (p != q) {vs++;add_edge(vs, p), add_edge(vs, q);val[vs] = w;par[p] = par[q] = vs;}}// 处理森林的情况for (int i = 1; i <= N; ++i) add_edge(0, Find(i));val[0] = -1;
}void dfs(int u) {for (int i = Head[u]; i; i = Next[i]) {int v = To[i];if (depth[v] != -1) continue;depth[v] = depth[u] + 1;fa[v][0] = u;for (int j = 1; ( 1 << j ) <= depth[v]; ++j)fa[v][j] = fa[fa[v][ j - 1 ]][j - 1];dfs(v);}
}void Tarjan(int u) {for (int i = Head[u]; i; i = Next[i]) {int v = To[i];if (vis[v] == true) continue;Tarjan(v);par2[v] = u;vis[v] = true;}for (int i = Head2[u]; i; i = Next2[i]) {int v = To2[i], n = Num[i];if (vis[v]) ans[n] = Find2(v);}
}void solve() {Kruskal();depth[0] = 0;dfs(0);for (int i = 1; i <= Q; ++i)add_query(X[i], Y[i], i),add_query(Y[i], X[i], i);Tarjan(0);for ( int i = 1; i <= Q; ++i )printf("%d\n", val[ans[i]]);
}int main() {init();solve();return 0;
}

- LCT 维护最小生成树

还是同一个问题,最小生成树一种边权图,而 LCT 是一种维护点权的数据结构。

老套路,直接拆点。我们可以直接把所有边对应的点建好。然后每次断边时断掉两条边,连边时连上两条边。

再看一道简单的模板题:

[WC2006] 水管局长

有一张 n 个点,m 条边的图,边有边权。你需要动态维护两种操作:

  • 某一条边消失。
  • 询问 u 到 v 的最小瓶颈路。

将询问翻转,即从后往前做。然后就变成了动态加边的最小生成树问题,询问时相当于问 u, v 在最小生成树的路径上最大权值。直接拆点用 LCT 维护即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;const int MaxN = 1000 + 5, MaxM = 100000 + 5, MaxQ = 100000 + 5;
const int MaxV = 101000 + 5;int N, M, Q;
struct edge { int u, v, w, id; bool ok; } E[MaxM];
int Opt[MaxQ], X[MaxQ], Y[MaxQ];
int Mp[MaxN][MaxN], par[MaxN];
int st[MaxQ], tp;struct LCT {#define lson ch[0]
#define rson ch[1]int fa[MaxV], ch[2][MaxV];int val[MaxV], maxid[MaxV];bool rev[MaxV];inline int getson(int x, int f) { return rson[f] == x; }inline void reverse(int x) { swap(lson[x], rson[x]); }inline bool is_root(int x) { return lson[fa[x]] != x && rson[fa[x]] != x; }inline void update(int x) {int ls = lson[x], rs = rson[x];if (val[maxid[ls]] > val[maxid[rs]]) maxid[x] = maxid[ls];else maxid[x] = maxid[rs];if (val[x] > val[maxid[x]]) maxid[x] = x;}inline void push_down(int x) {if (rev[x] == true) {reverse(lson[x]); reverse(rson[x]);rev[lson[x]] = !rev[lson[x]]; rev[rson[x]] = !rev[rson[x]];rev[x] = false;}}inline void rotate(int x) {int f = fa[x], g = fa[f];int l = getson(x, f);if (is_root(f) == false) ch[getson(f, g)][g] = x;if (ch[l ^ 1][x] != 0) fa[ch[l ^ 1][x]] = f;fa[x] = g; fa[f] = x;ch[l][f] = ch[l ^ 1][x]; ch[l ^ 1][x] = f;update(f);}void erase_tag(int x) {if (is_root(x) == false) erase_tag(fa[x]);push_down(x);}inline void splay(int x) {erase_tag(x);while (is_root(x) == false) {int f = fa[x], g = fa[f];if (is_root(f) == false) {if (getson(f, g) == getson(x, f)) rotate(f);else rotate(x);}rotate(x);}update(x);}inline void access(int f) {int x = 0;while (f != 0) {splay(f); rson[f] = x;update(f);x = f, f = fa[f];}}inline void make_root(int x) {access(x); splay(x);rev[x] = !rev[x]; reverse(x);}inline void split(int x, int y) {make_root(x);access(y); splay(y);}inline void link(int x, int y) {make_root(x);fa[x] = y;}inline void cut(int x, int y) {split(x, y);fa[x] = lson[y] = 0;update(y);}
} T;void init() {scanf("%d %d %d", &N, &M, &Q);for (int i = 1; i <= M; ++i) scanf("%d %d %d", &E[i].u, &E[i].v, &E[i].w);for (int i = 1; i <= Q; ++i) scanf("%d %d %d", &Opt[i], &X[i], &Y[i]);for (int i = 1; i <= N; ++i) par[i] = i;
}inline bool cmp(edge x, edge y) { return x.w < y.w; }
int Find(int x) { return x == par[x] ? x : par[x] = Find(par[x]); }void solve() {sort(E + 1, E + 1 + M, cmp);for (int i = 1; i <= M; ++i) {E[i].ok = true;E[i].id = i + N;Mp[E[i].u][E[i].v] = Mp[E[i].v][E[i].u] = i;T.val[E[i].id] = E[i].w;}for (int i = 1; i <= Q; ++i) {if (Opt[i] == 1) continue;int e = Mp[X[i]][Y[i]];E[e].ok = false;}for (int i = 1; i <= M; ++i) {int u = E[i].u, v = E[i].v;int p = Find(u), q = Find(v);if (E[i].ok == true && p != q) {T.link(E[i].id, u); T.link(E[i].id, v);par[p] = q;}}for (int i = Q; i >= 1; --i) {int opt = Opt[i], x = X[i], y = Y[i];if (opt == 1) {if (x == y) {st[++tp] = 0;continue;}T.split(x, y);st[++tp] = T.val[T.maxid[y]];} else {int m = Mp[x][y];T.split(x, y);int e = T.maxid[y] - N;if (E[e].w <= E[m].w) continue;T.cut(E[e].id, E[e].u); T.cut(E[e].id, E[e].v);T.link(E[m].id, x); T.link(E[m].id, y);}}while (tp > 0) printf("%d\n", st[tp--]);
}int main() {init();solve();return 0;
}

五、总结

拆点和拆边是非常经典的图论技巧之一,而且写起来也非常方便,很容易上手。但缺点在于空间占用需要翻倍,使用时千千万万记得开两倍的数组空间(我才不会告诉你这种东西我写十次 RE 九次)。

byTweetuzkiby\ Tweetuzkiby Tweetuzki

【图论技巧】点边转化(拆点和拆边)相关推荐

  1. 【无标题】word技巧之对象转化为图片

    就算是处于疫情中,也不能阻止我们一心向学,闲暇之余,除了刷抖音打王者和Lol microsoft家族给我们办公提供了很多便利,开发了很多办公软件,如word和visio就很常用不如来学学一个方便好用的 ...

  2. 图论技巧 : 超级源点与超级汇点的建立

    1.什么是超级源点与超级汇点 (1)超级源点跟超级汇点是模拟出来的虚拟点,多用于图中 : <1>同时有多个源点和多个汇点,建立超级源点和超级汇点 <2>同时有多个源点和一个汇点 ...

  3. 0x6A.图论 - 网络流初步

    目录 一.网络流基本概念 二.最大流 1)Edmonds−KarpEdmonds-KarpEdmonds−Karp算法 luogu P2740草地排水 Edmonds-Karp增广路,最大流模板 2) ...

  4. 各种图论模型及其解答(转)

    原文转自Jelline blog http://blog.chinaunix.net/uid-9112803-id-411340.html 摘要: 本文用另一种思路重新组织<图论及其应用> ...

  5. 给定数组 求和等于固定值 算法_[见题拆题] 大厂面试算法真题解析 - 第一期开张...

    如今想要收获大厂offer,在面试的前几轮,总是躲不开算法这座大山. 常听人说,算法很难.这话没错.算法本身是是一个艰深的方向.但是算法题却有据可循.通过有针对性的学习和练习,我们完全可以掌握解题的基 ...

  6. Git学习总结(18)——让你成为Git和GitHub大神的20个技巧

    Git不仅是编程世界最流行的分布式版本控制系统,而且你还可以用它查找,分享以及优化你的代码.接下来就来看看怎样让Git和GitHub更好地为你服务吧. 尽管现在网上有很多Git的初学者教程,而且Git ...

  7. 果断收藏!Git和GitHub大神常用的20个技巧!

    果断收藏!Git和GitHub大神常用的20个技巧! Git不仅是编程世界最流行的分布式版本控制系统,而且你还可以用它查找,分享以及优化你的代码.接下来就来看看怎样让Git和GitHub更好地为你服务 ...

  8. 新手怎么做直播卖货?都有哪些卖货成交话术技巧?

    都说2020年是直播电商全面爆发的一年,受疫情影响,全民直播的盛况更是空前绝后.不管怎样,现在,直播带货已明显地成为所有商家的共识. 直播带货虽火爆,但分化明显.头号主播有流量,有议价权,每场直播的销 ...

  9. 工具及方法 - 斗地主技巧

    斗地主游戏起源 斗地主是流行于湖北武汉.汉阳一带的一种扑克游戏.游戏需由3个玩家进行,用一副54张牌(连鬼牌),其中一方为地主,其余两家为另一方,双方对战,先出完牌的一方获胜.斗地主起源于湖北武汉汉阳 ...

最新文章

  1. 【MATLAB】符号数学计算(八):符号分析可视化
  2. 大数据处理时用到maven的repository
  3. 【Web安全】关于通过木马控制目标和使用中国菜刀拿webshell的应用
  4. ubuntu rar文件乱码
  5. java knn kd树_KNN算法之KD树(K-dimension Tree)实现 K近邻查询
  6. 音视频技术开发周刊 | 158
  7. mysql快照过久_Oracle 快照(snapshot) 管理
  8. 【玩味西班牙】之一:初识餐前小吃——达帕斯(TAPAS)
  9. 在Recyclerview使用GlideAPP加载大量图片导致内存溢出(oom)
  10. linux文件管理 - 系统文件属性
  11. python---之super()继承,解决钻石继承难题
  12. 七日杀a17服务器修改,七日杀a17作弊指令
  13. go语言io reader_Golang io.TeeReader()用法及代码示例
  14. 1、黑塞矩阵Hessian matrix
  15. 研发质量管理工作经验总结(五)----关于流程建设的思考
  16. 使用DeepAR实现股价预测
  17. centOS6.5中静默安装oracle 11gR2
  18. 一篇让你熟练掌握Google Guava包(全网最全)
  19. 正则表达式-JavaScript
  20. 对薛兆丰经济学思维的研究:价格的教益

热门文章

  1. 我仅使用到的dd if
  2. 初始Java DVD项目
  3. HDU 5734 Acperience
  4. 使用JavaMail发送邮件
  5. ADMT3.2域迁移之Server2003至Server2012系列(八)生成密钥文件及安装密码迁移工具...
  6. 实用javaScript技术-屏蔽总结
  7. struts2 iterator list中对象的list 双层迭代
  8. 利用Python基础代码语句,实现2G时代文字小游戏,世界如此简单
  9. python函数拟合不规则曲线_python 对任意数据和曲线进行拟合并求出函数表达式的三种解决方案...
  10. mysql常见内置函数_MySQL常用内置函数