Luogu P4542 [ZJOI2011] 营救皮卡丘

Luogu P4542 [ZJOI2011] 营救皮卡丘

注意到什么叫

两 面 包 夹 芝 士

这个是 最优解


这个是 最劣解

这究竟是怎么一回事呢?请看下文

挺有趣的这道题,我们先来

分析一下限制

最基础的就是 每个点都需要经过 这一点,并且要求 总路程最小

很容易想到的就是 路径覆盖问题,进而可以尝试 费用流 去求解

在有向图 \(G\) 中,设 \(P\) 是一个 简单路(顶点不相交)集合

如果 \(G\) 中的 每个顶点 都在 \(P\) 中的 一条路上,那么 \(P\) 就是 \(G\)一个路径覆盖

路径覆盖问题 就是 在 \(G\)求得特殊的的路径覆盖 \(P\) 的问题


例如 最小路径覆盖问题,注意到这里的最小不是指 最小权值,而是 路径条数最少

这类问题的基本思路就是 将每个点拆成两个点,建立 二分图

在原图上 有边的点对应的二分图,左右 / 右左 连边,求解(最小费用)最大匹配

难就难在这题有两个 特殊限制,第一个是 有 \(k\) 个人,也就是 最多同时走 \(k\)

但只是这个还是好解决的,我们可以限制 源点起点(对应的左部点)的流量

但是注意到另一个条件,即 到达点 \(K\) 时,必须已经经过过点 \(1 \sim K - 1\)

当时我就感觉 有点难搞啊,想了一会儿,考虑到这种 条件递进 的关系

于是有了 分层图 的想法(埋下伏笔


最劣解是怎么来的

这种想法 确实非常直观,只是...需要 大大大力卡常 + 代码复杂

有一种 \(ZJOI \to Ynoi\) 的美

我们可以直接 暴力把点分成 \(N\) 层,每层每个点向下一层对应点连单向边(保证不会走回来)

注意到 每个点必须经过一次,而且得 按顺序经过

于是我们可以对 \(i\) 个点从第 \(i\) 层连向 \(i + 1\) 的边 做一个 下界流量为 \(1\) 的限制

这里 可以直接用 上下界网络流 的套路

也可以 \(u \to v\) 连一条 容量 \(1\),费用 \(- \inf\) 的边,连一条 容量 \(\inf\),费用 \(0\) 的边

那么求解最小费用时,显然 费用 \(-\inf\) 的边会被走到,最后 总费用 加上 \(N\)\(\inf\) 即可

而其他边 不做限制,此时我们就已经保证 到第 \(i\) 层时,必然经过 \(1 \sim i - 1\) 的点

之后再来考虑 原图上的边,我们钦定原图有边 \((u, v)\)\(u < v\)

于是我们显然可以在每一层 层内连接 \(u \to v\) 的边

而由于 \(v > u\),为了防止破坏条件,我们需要保证 \(v\) 被炸掉之后 才能 往 \(v\) 小的点走

也就是在第 \(v\) 层之后,我们才连接 \(v \to u\) 的边

否则可能先炸掉 \(v\) 再炸掉 \(u\) 费用更少,但显然不合题意

虽然说这里可以在 \(i\) 就直接跳到 后面的点 \(j ~ (j > i)\)

但由于我们保证在 \(j\) 层之前\(j\) 不能 “往回跳”\(j\) 小的点

故而这种情况实际上相当于 一个人在第 \(i\) 个点,决定了去炸 \(j\)(以及后面的点)

但是其 不会途径 \(j\) 去炸 \(1 \sim j - 1\) 中的点

所以只需要等待其他人把 \(1 \sim j - 1\) 炸完再动即可,没有破坏条件

这里附赠一个样例,手玩一下有助于理解上面这段抽象的东西

5 7 2

0 1 100
1 2 3
0 3 1
3 2 1
2 4 2
3 4 100
2 5 1
    
Ans = 108

最后把第 \(N\) 层的 每个点连向汇点 \(T\)大功告成

交上去,你不得不承认这玩意儿是对的,但是 \(TLE + MLE ~ 70 ~ pts\)

于是开始了 漫长的卡常

\(2024.03.07 ~~ 21:47 \to 70 ~ pts\)

发现 值域很小,于是把 long long改成 int,快了一些,不 \(MLE\)

\(2024.03.08 ~~ 11:12 \to 80 ~ pts\)

发现在前 \(i\) 层时,每层建一组 “向后走” 的边 十分浪费,实质上一共 只需要一组,省一半边

\(2024.03.08 ~~ 12:07 \to 90 ~ pts\)

注意到如果存在点 \(A, B, C\),满足 \(A < B < C\)\(Dis (A, B) + Dis (B, C) < Dis (A, C)\)

那么边 \((A, C)\) 可以松弛,于是用 \(Bellman-Ford\) 遍历所有可以松弛的边

\(2024.03.08 ~~ 12:27 \to AC\)

发现上文中的边 \((A, C)\) 其实 完全没用,于是在建分层图的时候可以 忽略这些边

总算过了...

然后一看提交记录... 最劣解,主要是这样建图 边数的级别\(O(N M)\),或说 \(O(N ^ 3)\)

即是砍掉了很多,在最终 \(AC\) 的版本上,最大的点还是会建出 \(742233\) 条边,十分喃伻

仔细想想,发现 点数 / 边数上可以少乘个 \(N\)... 这里先给个 劣质的代码

#include <bits/stdc++.h>

using namespace std;

namespace MCMF {
	
    const int MAXN = 50005;
    const int MAXM = 20000;
	const int INF = 1e5; // Don't Be Too Large
	
	struct Edge {
		int u, v, nxt, f, w;
	} E[MAXM * 38];
	
	int H[MAXN], tot = 1;
	
	inline void Add_Edge (const int u, const int v, const int f, const int w) {
		E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
		E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
	}
	
	int Pre[MAXN];
	int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
	int Now = 0, S = 31451, T = 49198;
	
	inline void Init_ZCT (const int x, const int e, int Nod = 1) {
		fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
		for (int i = H[x]; i; i = E[i].nxt) 
			if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
	}
	
	inline int Sum (const int x) {
		if (Tag[x] == Now) return Pre[x];
		Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
		return Pre[x];
	}
	
	inline int Push_Flow (const int x) {
		int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
		++ Now;
		while (rt) Tag[rt] = Now, rt = fa[rt];
		while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
	
		int F = E[x].f, Cost = 0;
		for (int u = E[x].u; u != lca; u = fa[u]) {
			Cir[++ Cnt] = fe[u];
			if (F > E[fe[u]].f) 
				F = E[fe[u]].f, Del = u, P = 0;
		}
	
		for (int u = E[x].v; u != lca; u = fa[u]) {
			Cir[++ Cnt] = fe[u] ^ 1;
			if (F > E[fe[u] ^ 1].f)
				F = E[fe[u] ^ 1].f, Del = u, P = 1;
		}
		Cir[++ Cnt] = x;
		
		for (int i = 1; i <= Cnt; ++ i) 
			Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
		
		if (P == 2) return Cost;
		int u = E[x].u, v = E[x].v;
		if (P == 1) swap (u, v);
		int Lste = x ^ P, Lstu = v, Tmp;
		
		while (Lstu != Del) {
			Lste ^= 1, -- Tag[u];
			swap (fe[u], Lste);
			Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
		}
		return Cost;
	}
	
	int MinC = 0;
	
	inline int Simplex () {
		Add_Edge (T, S, INF, - INF);
		Init_ZCT (T, 0, ++ Now);
		Tag[T] = ++ Now, fa[T] = 0;
		bool Run = 1;
		
		while (Run) {
			Run = 0;
			for (int i = 2; i <= tot; ++ i) 
				if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
					MinC += Push_Flow (i), Run = 1;
		}
		
		MinC += E[tot].f * INF;
		return E[tot].f;
	}
}

namespace Value {
	using namespace MCMF;
	const int MAXP = 155;
	int D[MAXP][MAXP];
	int N, M, K, u, v, c;
	int A[MAXN];
	int VS = 31455, VT = 49199;
	
	inline int G (const int x, const int f) {
		return x + f * MAXP;
	}
	
	inline void Solve () {
		
		cin >> N >> M >> K, ++ N;
		
		for (int i = 1; i <= N; ++ i) {
			for (int j = 1; j <= i; ++ j)
				Add_Edge (G (j, i - 1), G (j, i), K, 0);
			Add_Edge (S, G (i, i), 1, 0), Add_Edge (G (i, i - 1), T, 1, 0);
		}
		
		memset (D, 63, sizeof D);
		
		for (int i = 1; i <= M; ++ i) {
			cin >> u >> v >> c, ++ u, ++ v;
			if (u > v) swap (u, v);
			D[u][v] = min (D[u][v], c);
		}
		
		for (int i = 1; i < N; ++ i)
			for (int j = i + 2; j <= N; ++ j)
				for (int k = i + 1; k < j; ++ k)
					if (D[i][j] < 10000 && D[i][k] + D[k][j] < D[i][j])
						D[i][j] = D[i][k] + D[k][j];
		
		for (int i = 1; i < N; ++ i)
			for (int j = i + 2; j <= N; ++ j)
				for (int k = i + 1; k < j; ++ k)
					if (D[i][j] < 10000 && D[i][k] + D[k][j] == D[i][j])
						D[i][j] = 10001;
		
		for (u = 1; u <= N; ++ u)
			for (v = u; v <= N; ++ v)
				if (D[u][v] < 10000) {
					c = D[u][v];
					Add_Edge (G (u, u - 1), G (v, v - 1), K, c);
					Add_Edge (G (u, v - 1), G (v, v - 1), K, c);
					for (int k = v; k <= N; ++ k)
						Add_Edge (G (v, k), G (u, k), K, c), Add_Edge (G (u, k), G (v, k), K, c);
				}
		
		for (int i = 1; i <= N; ++ i) Add_Edge (G (i, N), VT, K, 0);
		
		Add_Edge (VS, G (1, 0), K, 0), Add_Edge (VT, VS, K, 0);
		
		int Ans = Simplex ();
		
		cerr << tot << endl;
		cerr << Ans << endl;
		cout << MinC << endl;
	}
}

int main () {
	
	Value::Solve ();
	
	return 0;
}

最优解是怎么来的

我们注意到 第三个条件 本质上就是 不要 先炸了后面的再回头炸前面的

而我们刚刚 卡常的倒数第二步 抓住了一个关键,也就是 最短路

事实上,我们可以用 \(Bellman-Ford\) 预处理出 任意两点间 不经过后面点最短路径

于是我们可以 回到二分图,同一个点 拆成左右两点,连边,保证下界为 \(1\)

按照路径覆盖的套路,设 \(Dis (u, v) = c\),我们将 \(u\)右部点\(v\)左部点 连接,费用为 \(c\)

注意到为防止 ”回头炸前面的“,我们需要保证此处 \(u < v\)

由于前面我们已经处理出 任意两点距离,故这里实际上任意 \(u, v\) 之间均有边,与原图无关

也就是 点号较小的点对应的右部点点号较大的点对应的左部点单向边

这样也就不可能出现 回头 的情况

但是可能有人会有疑问,如果 需要借道前面走过的点 时,正确性会不会有问题

我们注意到此时的 一条边 实际上代表的是 \(Bellman-Ford\) 处理出来的 一条路径

这里路径是 包括了 借道的情况,比如前面给的小样例中的 最优情况

就会存在一条 \(3 \to 2 \to 4\)需要借道 的路径

而反映到 二分图 中,我们预处理出的 \(3, 4\)最短距离 就是 \(3\)\(3 \to 2 \to 4\) 的长度)

于是会有 \(3\) 的右部点 连向 \(4\) 的左部点 的 一条费用为 \(3\) 的边,也就代表了这种情况

故而容易知道,正确性保证

于是直接连边做就行了,这样边数显然只有 \(O(N ^ 2)\) 的级别

在最大的点上实际建出了 \(23561\) 条边,是上一种方法的 \(\dfrac {1} {32}\) 左右,十分的快

#include <bits/stdc++.h>

using namespace std;

namespace MCMF {
	
    const int MAXN = 50005;
    const int MAXM = 20000;
	const int INF = 1e5; // Don't Be Too Large
	
	struct Edge {
		int u, v, nxt, f, w;
	} E[MAXM * 2];
	
	int H[MAXN], tot = 1;
	
	inline void Add_Edge (const int u, const int v, const int f, const int w) {
		E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
		E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
	}
	
	int Pre[MAXN];
	int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
	int Now = 0, S = 31451, T = 49198;
	
	inline void Init_ZCT (const int x, const int e, int Nod = 1) {
		fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
		for (int i = H[x]; i; i = E[i].nxt) 
			if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
	}
	
	inline int Sum (const int x) {
		if (Tag[x] == Now) return Pre[x];
		Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
		return Pre[x];
	}
	
	inline int Push_Flow (const int x) {
		int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
		++ Now;
		while (rt) Tag[rt] = Now, rt = fa[rt];
		while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
	
		int F = E[x].f, Cost = 0;
		for (int u = E[x].u; u != lca; u = fa[u]) {
			Cir[++ Cnt] = fe[u];
			if (F > E[fe[u]].f) 
				F = E[fe[u]].f, Del = u, P = 0;
		}
	
		for (int u = E[x].v; u != lca; u = fa[u]) {
			Cir[++ Cnt] = fe[u] ^ 1;
			if (F > E[fe[u] ^ 1].f)
				F = E[fe[u] ^ 1].f, Del = u, P = 1;
		}
		Cir[++ Cnt] = x;
		
		for (int i = 1; i <= Cnt; ++ i) 
			Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
		
		if (P == 2) return Cost;
		int u = E[x].u, v = E[x].v;
		if (P == 1) swap (u, v);
		int Lste = x ^ P, Lstu = v, Tmp;
		
		while (Lstu != Del) {
			Lste ^= 1, -- Tag[u];
			swap (fe[u], Lste);
			Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
		}
		return Cost;
	}
	
	int MinC = 0;
	
	inline int Simplex () {
		Add_Edge (T, S, INF, - INF);
		Init_ZCT (T, 0, ++ Now);
		Tag[T] = ++ Now, fa[T] = 0;
		bool Run = 1;
		
		while (Run) {
			Run = 0;
			for (int i = 2; i <= tot; ++ i) 
				if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
					MinC += Push_Flow (i), Run = 1;
		}
		
		MinC += E[tot].f * INF;
		return E[tot].f;
	}
}

namespace Value {
	using namespace MCMF;
	const int MAXP = 155;
	int D[MAXP][MAXP];
	int N, M, K, u, v, c;
	int A[MAXN];
	int VS = 31455, VT = 49199;
	
	inline int G (const int x, const int f) {
		return x + f * MAXP;
	}
	
	inline void Solve () {
		
		cin >> N >> M >> K, ++ N;
		
		for (int i = 1; i <= N; ++ i) {
			Add_Edge (G (i, 0), G(i, 1), 1, - 1e6);
			Add_Edge (G (i, 0), G (i, 1), K, 0);
			Add_Edge (G (i, 1), T, K, 0);
		}
		
		memset (D, 63, sizeof D);
		
		for (int i = 1; i <= M; ++ i) {
			cin >> u >> v >> c, ++ u, ++ v;
			if (u > v) swap (u, v);
			D[v][u] = D[u][v] = min (D[u][v], c);
		}
		
		for (int i = 1; i < N; ++ i)
			for (int j = i; j <= N; ++ j)
				for (int k = 1; k < j; ++ k)
					if (D[i][k] + D[k][j] < D[i][j])
						D[j][i] = D[i][j] = D[i][k] + D[k][j];
	
		for (int i = 1; i < N; ++ i)
			for (int j = i + 1; j <= N; ++ j)	
				Add_Edge (G (i, 1), G (j, 0), K, D[i][j]);
		
		Add_Edge (S, G (1, 0), K, 0);
		
		int Ans = Simplex ();
		cerr << Ans << endl;
		cout << (MinC + N * 1000000) << endl;
	}
}

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	Value::Solve ();
	
	return 0;
}
posted @ 2024-03-08 19:30  FAKUMARER  阅读(13)  评论(2编辑  收藏  举报