关于 图论建模 的一些技巧

分层图(其实就是 拆点 的一种)

分层图在 最短路 中经常用到。

直观上讲,就是将一个图复制 k 倍,互相是平行的,即互不影响,分层图 两两之间 会有 决策边 相连。

这就等价于要在一个图上进行 k 次决策,对于每次决策,不影响图的结构,只影响目前的状态或代价。一般将决策前的状态和决策后的状态之间连接一条权值为决策代价的边,也就是决策边。

一般有两种方法:

  • 建图直接建 k + 1 层;
  • 数组多开一维(多维)记录决策信息;(与dp思想相关)

Part Ⅰ

P1948 [USACO08JAN] Telephone Lines S

题目说明可以进行 k 次免费决策,考虑直接建 k + 1 层图。

对于一条路径 (x, y, w) ,如果考虑决策免费,那就等价于向下一状态连一条 0 边权的单向边,即 (x, y + n, 0)

建图完就可以直接 dij 跑图,

同时注意到所求答案要求是 一条满足条件的路径上的最大边权的最小值

也就是,对于当前访问到的点 y,(x, y, w) ,如果 1 ~ y 上原先记录的最小最大边权比前驱点 x 所记录的以及当前 w 比较都大了,

\(dist[y] > \max(dist[x],~w)\) ,说明可以更新 \(dist[y] = \max(dist[x],~w)\)

这样就做到了 最小化最大边权的收敛


注意:可能存在 k 次免费决策不必全部用完就可到达终点的情况,所以 k 次决策可能不全使用,因此对每个点要考虑当前状态不使用决策直接转移到下一状态,即连 (i, i + n, 0)

边数计算一下是要 \(2(k + 1)M + 2Mk + kN = 4MN + N^2\),大概 5e7 的样子,不会超空间。

所以,很重要的是 分层图建图要考虑建图思路会不会超空间

code
#include <bits/stdc++.h>
#define re register int 
#define max(x, y) (x > y ? x : y)
using namespace std;
typedef pair<int, int> pii;
const int N = 1e6 + 10, M = 5e7 + 10, inf = 0x3f3f3f3f;
struct edge
{
	int to, w, next;
}e[M];
int top, h[N], dist[N];
int n, m, k;
bool vis[N];
priority_queue< pii, vector<pii>, greater<pii> > q;

inline void add(int x, int y, int w)
{
	e[++ top] = (edge){y, w, h[x]};
	h[x] = top;
}

inline void build(int a, int b, int c)
{
	add(a, b, c); 
	add(b, a, c);
	for (re i = 1; i <= k; i ++)
	{
		add(i * n + a, i * n + b, c);
		add(i * n + b, i * n + a, c);
		
		add((i - 1) * n + a, i * n + b, 0);
		add((i - 1) * n + b, i * n + a, 0);
	}		
}

inline bool dijkstra()
{
	memset(vis, false, sizeof(vis));
	for (re i = 0; i <= (k + 1) * n; i ++) dist[i] = inf;
	
	dist[1] = 0;
	q.push(make_pair(0, 1));
	
	while (!q.empty())
	{
		int x = q.top().second; q.pop();
		if (vis[x]) continue;
		vis[x] = true;
		
		for (re i = h[x]; i; i = e[i].next)
		{
			int y = e[i].to, w = e[i].w;
			if (dist[y] > max(dist[x], w))
			{
				dist[y] = max(dist[x], w);
				q.push(make_pair(dist[y], y));
			}
		}
	}
	
	return (dist[(k + 1) * n] == inf ? false : true);
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> m >> k;
	while (m --)
	{
		int a, b, c; cin >> a >> b >> c;
		build(a, b, c);
	}
	for (re i = 1; i <= n; i ++)
		for (re j = 1; j <= k; j ++)
			add((j - 1) * n + i, j * n + i, 0);
			
	if (dijkstra()) cout << dist[(k + 1) * n] << '\n';
	else cout << "-1\n";
	
	return 0;
}

例题:

P2939 [USACO09FEB] Revamping Trails G
P4822 [BJWC2012] 冻结
P4568 [JLOI2011] 飞行路线

P1073 [NOIP2009 提高组] 最优贸易

一道很经典的分层图,

考虑一次买入一次卖出共两次决策,买入建负权边,卖出建正权边,同层国家可以任意经过且无花费。

image

答案就是从一层 1 号点到三层 n 号点的最长路,

此处最长路可以将边权正负调转 转化成求最短路跑 spfa,再将答案转正数即可。

code
#include <bits/stdc++.h>
#define re register int
using namespace std;
const int N = 3e5 + 10, M = 2e6 + 10, inf = 0x3f3f3f3f;
struct edge
{
	int to, w, next;
}e[M];
int top, h[N], dist[N];
int n, m, w[N], x[N], y[N], opt[N];
bool vis[N];
queue<int> q;

void add(int x, int y, int w)
{
	e[++top] = (edge){y, w, h[x]};
	h[x] = top;
}

void build()
{
	for (re i = 1; i <= n; i ++)
		add(i, i + n, w[i]);
	for (re i = 1; i <= m; i ++)
	{
		add(x[i] + n, y[i] + n, 0);
		if (opt[i] == 2) add(y[i] + n, x[i] + n, 0);
	}
	
	for (re i = n + 1; i <= n * 2; i ++)
		add(i, i + n, -w[i - n]);
	n *= 2;
	for (re i = 1; i <= m; i ++)
	{
		add(x[i] + n, y[i] + n, 0);
		if (opt[i] == 2) add(y[i] + n, x[i] + n, 0);
	}
	n /= 2;
	
	n *= 3;
}

void spfa()
{
	memset(vis, false, sizeof(vis));
	for (re i = 0; i <= n; i ++) dist[i] = inf;
	
	dist[1] = 0;
	q.push(1);
	vis[1] = true;
	
	while (!q.empty())
	{
		int x = q.front(); q.pop();
		vis[x] = false;
		
		for (re i = h[x]; i ; i = e[i].next)
		{
			int y = e[i].to, w = e[i].w;
			if (dist[y] > dist[x] + w)
			{
				dist[y] = dist[x] + w;
				if (!vis[y])
				{
					vis[y] = true;
					q.push(y);
				}
			}
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> m;
	for (re i = 1; i <= n; i ++) cin >> w[i];
	for (re i = 1; i <= m; i ++)
	{
		cin >> x[i] >> y[i] >> opt[i]; 
		add(x[i], y[i], 0);
		if (opt[i] == 2) add(y[i], x[i], 0);
	}
	build();
	spfa();
	cout << -dist[n];
	
	return 0;
}

Part Ⅱ

P1266 速度限制

直观的,如果想要得到最短路径,贪心地想:

对于有限速的路径,时间(即边权)确定,不考虑;

对于无限速的路径,总是以前驱路径中速度最快的路径作为当前路径的速度;

一开始我就是这样做的

code
#include <bits/stdc++.h>
#define re register int 
using namespace std;
typedef tuple<double, int, double> T;
const int N = 200, M = 4e4 + 10;
const double inf = 1000000000.00;
struct edge
{
	int to, next;
	double v, L;
}e[M];
int top, h[N];
double dist[N];
int n, m, t, pre[N], res[N], idx;
bool vis[N];
priority_queue< T, vector<T>, greater<T> > q;

inline void add(int x, int y, double v, double L)
{
	e[++ top].to = y;
	e[top].next = h[x];
	e[top].v = v;
	e[top].L = L;
	h[x] = top;
}

inline void dijkstra()
{
	memset(vis, false, sizeof(vis));
	for (re i = 0; i <= n; i ++) dist[i] = inf;
	
	dist[0] = 0.00;
	q.push(make_tuple(0.00, 0, 70.00));
	
	while (!q.empty())
	{
		int x = get<1>(q.top());
		double v = get<2>(q.top());
		q.pop();
		if (vis[x]) continue;
		vis[x] = true;
		
		for (re i = h[x]; i; i = e[i].next)
		{
			int y = e[i].to;
			double vv = e[i].v, L = e[i].L, cost;
			if (vv == 0.00) cost = L / v;
			else cost = L / vv;
			
			if (dist[y] > dist[x] + cost)
			{
				dist[y] = dist[x] + cost;
				pre[y] = x;
				q.push(make_tuple(dist[y], y, (vv == 0.00 ? v : vv)));
			}
		}
	}
}

void path(int t)
{
	res[++ idx] = t;
	
	if (t == 0) return;
	path(pre[t]);
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> m >> t;
	while (m --)
	{
		int a, b;
		double c, d; cin >> a >> b >> c >> d;
		add(a, b, c, d);
	}
	dijkstra();
	path(t);
	for (re i = idx; i >= 1; i --)
		cout << res[i] << ' ';
	
	return 0;
}

结果发现样例都过不了,交上去居然也有 50pts ?...


所以,贪心地以最大速度作为无限速路径的速度是错误的。

同时有 \(V \leq 500\),这启发我们,可以把每个速度的状态都跑出来(扩展),打擂台个最优解即可。

扩展 \(dist[x,v]\) 表示以速度 \(v\) 到达点 \(x\) 的从 1 ~ n 的最快时间

讨论路径 -> x -> y,(x, y, v', L),有前驱数据 (x, v)

  • 若 v' > 0,\(dist[y,v'] = \min(dist[x,v] + L/v')\)

  • 若 v' = 0, \(dist[y,v] = \min(dist[x,v] + L/v)\)

对于终点 t,答案就是 \(\max\limits_{i=0}^{V}(dist[t,i])\),然后递归输出路径即可。

code
#include <bits/stdc++.h>
#define re register int 
using namespace std;
typedef tuple<double, int, double> T;
const int N = 200, M = 4e4 + 10, V = 500;
const double inf = 1000000000.00;
struct edge
{
	int to, next, v, L;
}e[M];
int top, h[N];
double dist[N][V + 10];
int n, m, t, pre[N][V + 10][2], idx;
bool vis[N][V];
priority_queue< T, vector<T>, greater<T> > q;

inline void add(int x, int y, double v, double L)
{
	e[++ top].to = y;
	e[top].next = h[x];
	e[top].v = v;
	e[top].L = L;
	h[x] = top;
}

inline void dijkstra()
{
	memset(pre, -1, sizeof(pre));
	memset(vis, false, sizeof(vis));
	for (re i = 0; i < n; i ++) 
		for (re j = 0; j <= V; j ++) dist[i][j] = inf;
	
	dist[0][70] = 0.00;
	q.push(make_tuple(0.00, 0, 70));
	
	while (!q.empty())
	{
		int x = get<1>(q.top()), v = get<2>(q.top());
		q.pop();
		if (vis[x][v]) continue;
		vis[x][v] = true;
		
		for (re i = h[x]; i; i = e[i].next)
		{
			int y = e[i].to;
			int vv = e[i].v, L = e[i].L;
			
			if (vv == 0)
			{
				if (dist[y][v] > dist[x][v] + 1.00 * L / v)
				{
					dist[y][v] = dist[x][v] + 1.00 * L / v;
					pre[y][v][0] = x;
					pre[y][v][1] = v;
					q.push(make_tuple(dist[y][v], y, v));
				}
			}
			else
			{
				if (dist[y][vv] > dist[x][v] + 1.00 * L / vv)
				{
					dist[y][vv] = dist[x][v] + 1.00 * L / vv;
					pre[y][vv][0] = x;
					pre[y][vv][1] = v;
					q.push(make_tuple(dist[y][vv], y, vv));
				}
			}
		}
	}
}

void path(int u, int vm)
{
	if (u == -1) return;
	path(pre[u][vm][0], pre[u][vm][1]);
	cout << u << ' ';
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> m >> t;
	while (m --)
	{
		int a, b;
		double c, d; cin >> a >> b >> c >> d;
		add(a, b, c, d);
	}
	dijkstra();
	int vm = 0;
	for (re i = 0; i <= V; i ++)
		if (dist[t][i] < dist[t][vm]) vm = i;
 	path(t, vm);
	
	return 0;
}

CF1473E Minimum Path

求单源“最短路”,不过一条路径,假设为 \(e_1, e_2...e_k\),不是求 \(\sum\limits_{i = 1}^{k}w_i\),而做了个变式:

\[\sum\limits_{i=1}^{k}w_i - \max\limits_{i=1}^{k}w_i + \min\limits_{i=1}^{k}w_i \]

可以发现,里边有 ∑wi 的部分,考虑还是求最短路。

对另外两部分考虑转化,对当前最短路径,要减去一个路径上最大边权值,加上一个路径上最小边权值。

那么问题可以转化为:不计最大边权,计两遍最小边权,其他累加,求最短路径

考虑设计约束状态,定义 \(dist[x, a, b]\),表示从 1 ~ x 路径上的最短路径,其中 \(a, b \in\{1,0\}\),分别表示当前状态 是否 已选择最大边权、两遍最小边权。

对边权为 w 的 u -> v,考虑状态转移方程:

\[\large\begin{cases} dist[v, a, b] &= \min(dist[v, a, b], dist[u, a, b] + w) \\ \\dist[v, 1, b] &= \min(dist[v, 1, b], dist[u, 0, b]) \\ \\dist[v, a, 1] &= \min(dist[v, a, 1], dist[u, a, 0] + 2\times w) \end{cases}\]

最后考虑极限情况,若该路径只有一条路径,那么最大权值就等于最小权值,此时答案就是 \(dist[i, 0, 0]\),而一般情况就是 \(dist[i, 1, 1]\),两者取 min 即可。

code
#include <bits/stdc++.h>
#define re register int 
#define min(x, y) (x < y ? x : y)
using namespace std;
typedef long long LL;
const int N = 2e5 + 10, M = 4e5 + 10;

struct T
{
	LL dist;
	int x, a, b;

	friend bool operator <(T i, T j)
	{
		return i.dist > j.dist;
	}
};

struct edge
{
	int to, w, next;
}e[M];
int top, h[N];

int n, m;
LL dist[N][2][2];
bool vis[N][2][2];

inline void add(int x, int y, int w)
{
	e[++ top] = (edge){y, w, h[x]};
	h[x] = top;
}

inline void dijkstra()
{
	priority_queue<T> q;
	
	memset(dist, 0x3f, sizeof(dist));
	
	dist[1][0][0] = 0;
	q.push({0, 1, 0, 0});
	
	while (!q.empty())
	{
		int u = q.top().x, a = q.top().a, b = q.top().b; 
		q.pop();
		
		if (vis[u][a][b]) continue;
		vis[u][a][b] = true;
		
		for (re i = h[u]; i; i = e[i].next)
		{
			int v = e[i].to, w = e[i].w;
			
			if (dist[v][a][b] > dist[u][a][b] + w)
			{
				dist[v][a][b] = dist[u][a][b] + w;
				q.push({dist[v][a][b], v, a, b});
			}
			
			if (a == 0 && dist[v][a ^ 1][b] > dist[u][a][b])
			{
				dist[v][a ^ 1][b] = dist[u][a][b];
				q.push({dist[v][a ^ 1][b], v, a ^ 1, b});
			}
			
			if (b == 0 && dist[v][a][b ^ 1] > dist[u][a][b] + 2 * w)
			{
				dist[v][a][b ^ 1] = dist[u][a][b] + 2 * w;
				q.push({dist[v][a][b ^ 1], v, a, b ^ 1});
			}
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> m;
	for (re i = 1; i <= m; i ++)
	{
		int a, b, c; cin >> a >> b >> c;
		add(a, b, c);
		add(b, a, c);
	}
	dijkstra();
	for (re i = 2; i <= n; i ++)
		cout << min(dist[i][0][0], dist[i][1][1]) << ' ';
	
	return 0;	
}

例题:

UVA11367 Full Tank?

小结

注意到这类题目有个很明显的共性,就是题目中有一个(或多个)大小比较小的约束条件,就像 k(k <= 1000)次决策,速度限制 v <= 500,甚至最优贸易只有买入、卖出两种决策,路径 max、min转化成 0/1 的决策问题。


虚点、虚边

对原图额外加上几个自己建立的“虚拟”的点,可以达到快速建图、快速求解,解决题目中对答案的一些限制,同时,虚边一般用来表示实点之间的一种关系,所以一般边权为 0

这种思想没有很套路化的东西,还是要结合具体题目考虑。


CF1775D Friendly Spiders

很明显,有 双向边 相连的点要满足 gcd(a, b) > 1,即两点的点权 不互质

如果直接暴力建图,就要 \(O(n^2\log n)\) 的时间,显然朴素建图跑最短路是不可行的。


很容易发现,在这种条件下建的图,

满足有这样一种很好的子图,子图上的所有点的点权都是有相同公因数的点(即题意),且两两直接都有边相连,即 完全图

考虑转换这样的完全图,给它一个代表,因为该子图上的点都有公因数,我们可以建 公因数虚点

该子图上的所有点向公因数虚点连边,构造出来的图显然也是能两两(可能间接)到达的,满足性质。

这样对所有点遍历,向公因数虚点建边,复杂度就可以降到 \(O(n\log n)\)

同时发现原图边权为 1,那么距离也就是深度,我们就可以直接在虚点、虚边、实点的图上跑 bfs 记录深度,同时记录路径即可。

image

存储路径点时,可以发现,因为整个图一定是由若干个完全图(以及一些没diao用的独立点)组成,

这说明假设从 s 到当前点为 i,在完全图 A 里,要走到 t,下一步一定会走到另一个完全图 B 里的 j 点,也就是说存储得到的路径点一定是形如 实点 -> 虚点 -> 实点 -> 虚点 ... 这样。

可以反证

假设处于 i 点,当前 A 中有 i, j,其中 j 包含于另一子图 B,

只考虑在 A 子图中最短路走法,若 A 中还有一点 k,

在完全图中,显然有路径 i -> j,i -> k,k -> j,显然根据三角形不等式有 i -> k -> j > i -> j

所以 i -> j 就是最优路径。

(好像没必要...)

注意空间,这里我建虚点采用 + n 的办法,那么实际上虚点编号最多可能到达 n + n,也就是 6e5

code
#include <bits/stdc++.h>
#define re register int 
using namespace std;
const int N = 1e6 + 10;

int n, s, t, dep[N];
vector<int> g[N], path;

inline void build(int a, int i)
{
	for (re j = 2; j * j <= a; j ++)
	{
		if (a % j == 0)
		{
			g[n + j].push_back(i);
			g[i].push_back(n + j);
			while (a % j == 0) a /= j;
		}
	}
	if (a > 1) 
	{
		g[n + a].push_back(i);
		g[i].push_back(n + a);
	}
}

inline void bfs()
{
	queue<int> q;
	
	dep[s] = 1;
	q.push(s);
	
	while (!q.empty())
	{
		int x = q.front(); q.pop();
		
		for (re i = 0; i < g[x].size(); i ++)
		{
			int y = g[x][i];
			if (!dep[y])
			{
				dep[y] = dep[x] + 1;
				q.push(y);
			}
		}
	}
}

inline void print()
{
	int u = t;
	path.push_back(u);
	
	while (u != s)
	{
		for (re i = 0; i < g[u].size(); i ++)
		{
			int v = g[u][i];
			if (dep[v] + 1 == dep[u])
			{
				u = v;
				path.push_back(u);
				break;
			}
		}
	}
	
	cout << path.size() / 2 + 1 << '\n';
	
	reverse(path.begin(), path.end());
	
	for (re i = 0; i < path.size(); i ++)
		if (path[i] <= n) cout << path[i] << ' ';		
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n;
	for (re i = 1; i <= n; i ++)
	{
		int a; cin >> a;
		build(a, i);
	}
	cin >> s >> t;
	bfs();
	
	if (!dep[t]) { cout << "-1\n"; return 0; }
	print();
	
	return 0;
}

例题:

CF1941G Rudolf and Subway
[ARC061E] すぬけ君の地下鉄旅行
CF1245D Shichikuji and Power Grid(最小生成树)


P1983 [NOIP2013 普及组] 车站分级

这题有很明显的拓扑关系:非停靠站等级 < 停靠站等级

有这样的不等关系,实际上我们就可以连一条边权为 1 的边来表示 a -> b,表示 b 的等级严格至少比 a 的等级大 1 级

那么对于每一列火车,我们可以把非停靠站放一边为左部 A,停靠站放一边为右部 B,

\(\forall x\in A,y\in B\),总有 x -> y,也就是会有 \(n^2\) 级别的建图

算一下,\(n,m \leq 1000\),每趟火车都要建就有 500 * 500 * 1000 = 250000000,肯定会爆空间

那么那么,这里就有一个很 nb 的优化建图方式,建虚拟中继点(自己瞎取的 qwq)

image

显然是等价的对吧,(有点像小学的连一连题目,把所有边都连到一起,这样老师也分辨不出我哪条连的哪条)

这样边数从 2.5e8 可以降到 (500 + 500) * 1000 = 1e6,perfect!

在图上跑个拓扑序,同时维护层级 level 即可。

code
#include <bits/stdc++.h>
#define re register int 

using namespace std;
const int N = 2e3 + 10, M = 1e6 + 10;

struct Edge
{
	int to, w, next;
}e[M];
int top, h[N], level[N];
int n, m, in[N], ans;
bool st[N];

inline void add(int x, int y, int w)
{
	e[++ top] = (Edge){y, w, h[x]};
	h[x] = top;
	
	in[y] ++;
}

inline void topsort()
{
	queue<int> q;
	for (re i = 1; i <= n + m; i ++)
	{
		if (!in[i]) q.push(i);
		if (i <= n) level[i] = 1;
	}
		
	while (!q.empty())
	{
		int x = q.front(); q.pop();
		
		for (re i = h[x]; i; i = e[i].next)
		{
			int y = e[i].to, w = e[i].w;
			if (-- in[y] == 0) 
			{
				level[y] = level[x] + w;
				q.push(y);
			}
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> m;
	for (re i = 1; i <= m; i ++)
	{
		memset(st, false, sizeof(st));
		
		int k; cin >> k;
		int s = n, t = 1;
		while (k --)
		{
			int x; cin >> x;
			s = min(s, x); t = max(t, x);
			st[x] = true;
		}
		
		int node = n + i;
		for (re i = s; i <= t; i ++)
		{
			if (!st[i]) add(i, node, 0);
			else add(node, i, 1);
		}
	}
	topsort();
	for (re i = 1; i <= n; i ++) ans = max(ans, level[i]);
	cout << ans << '\n'; 
	
	return 0;
}


虚拟源点、反图

这两种技巧很简单,就简单记录一下,知道用就可以了。

AcWing 1137. 选择最佳线路

就是从多个点向单点求最短路。

不可能对没给点分别跑一次 dijkstra,超时。

  • 一种方法是可以建反图,把单向边全部换向,再从单点向其他点跑一次最短路,由对称性易得,求出的最短路径是等效的;

  • 另一种方法就是建虚拟源点,对每个所给出的多个点与源点连一条由源点出发的 边权为 0 的单向边,从源点跑一次最短路,由于多建的边边权为 0,这样间接求得的子最短路一定也是最优的。


同类多源点最短路问题 的理解

给一个例子,比如现在有 n 个地点,m 条边,n 个地点中有 p 个地点是有超市的,求任意点到最近超市的距离( p ≤ n)

好像按照朴素地想法来看,这个问题是个多到多的最短路问题,难道要跑 p 次 dijkstra ?还是 floyd ?

其实不需要

可以这样转化,因为它不是要求一定是某个点到某个有超市的点的最短距离,而是任意点到一类点的最短距离

考虑转化,建立一个虚拟源点,连接所有 p 个节点,边权为 0,从虚拟源点跑一次 dijkstra 即可

对应到实际代码中,等价于就是将 p 个点都先入队

运用:P9432 [NAPC-#1] rStage5 - Hard Conveyors(虽然要加个树剖,但是板子 qwq)

\[\tt{upd~on~2024/10/18} \]


线段树优化建图

简单地讲,就是当遇到需要由点连向区间(也就是一个点集),或区间连向区间时,此时如果直接暴力建边将会是 \(O(mn^2)\)

而显然对于区间操作,我们考虑用线段树去维护它,以区间连向区间为例子

我们需要建两颗线段树,一颗出树,一颗入树

  • 对于入树,由儿子向父亲连 0 贡献边,因为如果子区间可以到达,则包含它的区间也必然能到达

  • 对于出树,由父亲向儿子连 0 贡献边,同理

  • 对于两棵树之间,入树向出树对应点连 0 贡献边,因为能到达,则必定能从这出发

这样对于每次连边,只需要连向对应线段树的点即可,由 \(O(n)\) 的边数降到 \(O(\log n)\)


CF786B Legacy

image

分三个编号点集,避免重复,线段树优化建图,然后跑 dij 即可

code
#include <bits/stdc++.h>
#define re register int 
#define lp p * 2
#define rp p * 2 + 1
//#define lp p << 1
//#define rp p << 1 | 1 玄学错误
#define n4 4 * n
#define n8 8 * n

using namespace std;
typedef long long LL;
typedef pair<LL, int> PII;
const int N = 9e5 + 10, M = 5e6 + 10;
const LL inf = 1e18 + 10;

struct Edge
{
	int to, next;
	LL w;
}e[M];
int idx, h[N];
struct Tree { int l, r; } t[N];
int n, q, s;
LL dis[N];
bool st[N];

inline void add(int x, int y, LL w)
{
	++ idx;
	e[idx].to = y, e[idx].w = w, e[idx].next = h[x];
	h[x] = idx;
}

inline void build(int p, int l, int r)
{
	t[p].l = l, t[p].r = r;
	if (l == r)
	{
		add(p, l + n8, 0), add(l + n8, p, 0);
		add(p + n4, l + n8, 0), add(l + n8, p + n4, 0);
		return;
	}
	add(p, lp, 0);
	add(p, rp, 0);
	add(lp + n4, p + n4, 0);
	add(rp + n4, p + n4, 0);
	
	int mid = (l + r) >> 1;
	build(lp, l, mid); build(rp, mid + 1, r);
}

inline void update(int p, int l, int r, int u, LL w, int type)
{
	if (l <= t[p].l && t[p].r <= r)
	{
		if (type) add(p + n4, u + n8, w);
		else add(u + n8, p, w);
		return;
	}
	int mid = (t[p].l + t[p].r) >> 1;
	if (l <= mid) update(lp, l, r, u, w, type);
	if (r > mid) update(rp, l, r, u, w, type);
}

inline void dij()
{
	priority_queue<PII, vector<PII>, greater<PII> > q;
	for (re i = 1; i <= n + n8; i ++) dis[i] = inf;
	
	dis[s + n8] = 0;
	q.push({0, s + n8});
	while (!q.empty())
	{
		int x = q.top().second; q.pop();
		if (st[x]) continue;
		st[x] = true;
		
		for (re i = h[x]; i; i = e[i].next)
		{
			int y = e[i].to; LL w = e[i].w;
			if (dis[y] > dis[x] + w)
			{
				dis[y] = dis[x] + w;
				q.push({dis[y], y});
			}
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> q >> s;
	build(1, 1, n);
	while (q --)
	{
		int op, u, v, l, r; LL w; 
		cin >> op;
		if (op == 1)
		{
			cin >> u >> v >> w;
			add(u + n8, v + n8, w);
		}
		if (op == 2) // u -> [l, r]
		{
			cin >> u >> l >> r >> w;
			update(1, l, r, u, w, 0);
		}
		if (op == 3) // [l, r] -> u
		{
			cin >> u >> l >> r >> w;
			update(1, l, r, u, w, 1);
		}
	}	
/*	for (re i = 1; i <= n + n8; i ++)
		for (re j = h[i]; j; j = e[j].next)
			cout << i << " --- " << e[j].to << ' ' << e[j].w << '\n';
*/		
	dij();
	for (re i = 1; i <= n; i ++) cout << (dis[i + n8] == inf ? -1 : dis[i + n8]) << ' ';
	cout << '\n';
	
	return 0;
}

练习:

P6348 [PA2011] Journeys(虚点,跑 01-bfs)

\[\tt{upd~on~2024/11/05} \]


差分约束

查分约束就是形如 \(x_i \leq x_j + c_k\) 的不等式组。

发现这种不等关系类似最短路问题中的三角形不等式 \(dist_y \leq dist_x + w\)

所以,可以将数学上的不等式建模为一条边权为 \(c_k\) 的有向边 \(x_j \to x_i\)

  • 可行解

那么,要求不等式组的一组可行解,等价于从一个 任意的 可以到达所有边的(除孤立点外,孤立点没有约束其的不等式)源点出发跑一遍单源最短路

所以,这里一般的 trick 就是建立一个虚拟源点 \(x_0\) 以边权为 0 连向所有点,等价于对所有原有点构造 \(x_i \leq x_0\),显然对结果是不会有影响的

  • 负环(不等式组无解)

举个例子:

image

得到 \(x_2 \leq x_2 + \sum{c_i}\)

如果后边那一坨常数 < 0,也就是出现负环,那么显然 \(x_2 \geq x_2 + \sum{c_i}\) 与原不等式矛盾。

那么如果不等号反过来,求最长路,无解的判定条件就变成正环了。

  • 最值解

有时查分约束并不单单要求可行解,可能会要求可行解中的最小值、最大值

当求最小值时,跑最长路;当求最大值时,跑最短路。这是显然的,可以用集合的角度解释:

image

还有一点,求最值是个绝对的值,而可行解是多个相对的值,所以要求最值一般就必须要有一个绝对关系的不等式,比如 \(x_i \leq k\)


一些别的东西

  • DAG 中,可以用拓扑排序 O(n + m) 直接扫描一遍得到单源最短路,从队头取出 x 后扫描邻边,得到 (x, y, w),做 \(d_y = \min(d_y, d_x + w)\) 即可,正确性是显然的。

  • 在含边权的无向图中,无自环、重边,以边权为约束条件求联通块,可以用并查集做,注意可能多条边存入会有点重复加入,要去重,瓶颈是 sort,每次点数是变化的,且所有边权跑不满,\(O(m\log m)\)

    code
    #include <bits/stdc++.h>
    #define re register int 
    using namespace std;
    typedef pair<int, int> PII;
    const int N = 2e6 + 10, M = 2e6 + 10;
    const int C = 1e6; 
    
    struct edge
    {
    	int to, w, next;
    }e[M];
    int top, h[N], dist[N], fa[N];
    int n, m;
    bool vis[N];
    
    vector<PII> g[N];
    vector<int> t;
    
    int find(int x)
    {
    	return fa[x] == x ? x : fa[x] = find(fa[x]);
    }
    
    inline void add(int x, int y, int w)
    {
    	e[++ top] = (edge){y, w, h[x]};
    	h[x] = top;
    }
    
    inline void build()
    {
    	for (re c = 1; c <= C; c ++)
    	{
    		if (g[c].empty()) continue;
    
    		t.clear();
    		for (auto x : g[c]) 
    			t.push_back(x.u), t.push_back(x.v);
    
    		sort(t.begin(), t.end());
    		t.erase(unique(t.begin(), t.end()), t.end());
    
    		for (auto i : t) fa[i] = i;
    
    		for (auto x : g[c])
    		{
    			if (find(x.u) != find(x.v)) fa[find(x.u)] = find(x.v);
    		}
    
    		for (auto i : t)
    		{
    			int res = find(i);
    
    			...
    
    		}
    
    	}
    }
    
    int main()
    {
    	ios::sync_with_stdio(false);
    	cin.tie(0); cout.tie(0);
    
    	cin >> n >> m;
    	for (re i = 1; i <= m; i ++)
    	{
    		int x, y, c; cin >> x >> y >> c;
    		g[c].push_back({x, y});
    	}
    	build();
    
    	return 0;
    }
    
  • 关于 bfs 及其变形从最短路角度理解

    • 记录深度,普通 bfs(\(O(n)\)),每个状态只访问一次,第一次入队时即为最优状态,相当于在边权为 1 的图上跑最短路

    • 每次扩展代价为 0/1,双端队列 bfs(\(O(n)\)),每个状态访问多次,只扩展一次,第一次出队即为最优状态,相当于在边权为 0 或 1 的图上跑最短路

    • 每次扩展代价为任意正值(>0),优先队列 bfs(\(O(n\log n)\)),每个状态访问多次,只扩展一次,第一次出队即为最优状态,相当于一般的最短路问题,也就是 dijistra。而基于迭代思想的 \(O(n^2)\) bfs,可以解决任意值,也就是已死的 spfa

  • lca 树上差分的前缀和还原,除了 dfs 再搜一遍,也可以 在初始化树的时候记录搜索序,循环一遍搜索序累加得到差分数组的前缀和

    法一:再次 dfs
    void cale(int u, int fa)
    {
    	for (re i = h[u]; i; i = e[i].next)
    	{
    		int v = e[i].to;
    
    		if (v == fa) continue;
    
    		cale(v, u);
    		d[u] += d[v];
    	}
    }
    
    法二:记录搜索序
    int seq[N], idx; // sequence
    
    void dfs(int u, int fa)
    {
    	dep[u] = dep[fa] + 1;
    	seq[++ idx] = u;
    
    	f[u][0] = fa;
    	for (re i = 1; i <= log2(n); i ++)
    		f[u][i] = f[f[u][i - 1]][i - 1];
    
    	for (re i = h[u]; i; i = e[i].next)
    	{
    		int v = e[i].to, w = e[i].w;
    
    		if (v == fa) continue;
    
    		dist[v] = dist[u] + w;
    		dfs(v, u);
    	}
    }
    
    inline void cale()
    {
    	for (re i = n; i >= 1; i --)
    	{
    		int x = seq[i];
    		d[f[x][0]] += d[x];
    	}
    }
    
posted @ 2024-05-20 21:39  Zhang_Wenjie  阅读(178)  评论(1)    收藏  举报