首页 > 其他分享 >P4542 [ZJOI2011] 营救皮卡丘

P4542 [ZJOI2011] 营救皮卡丘

时间:2024-03-08 19:33:52浏览次数:27  
标签:const int P4542 ZJOI2011 ++ Tag fe 皮卡丘 Now

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;
}

标签:const,int,P4542,ZJOI2011,++,Tag,fe,皮卡丘,Now
From: https://www.cnblogs.com/FAKUMARER/p/18061697

相关文章

  • TJ - 「ZJOI2011」道馆之战
    「ZJOI2011」道馆之战难度:2500\(1s,256MB\)一,题目:题目大意:给你一颗\(n\)个节点的树,每个节点有\(A,B\)两个区域,每个区域可以为障碍物/冰块,只能在冰块上行走,每次行走你可以走到相邻节点的同个区域,或当前节点的另一个区域(前提是这个区域可以走),现在有\(m\)个操作和询问,操作是修改......
  • 升级版皮卡丘
    importturtledefgetPosition(x,y):turtle.setx(x)turtle.sety(y)print(x,y)classPikachu:def__init__(self):self.t=turtle.Turtle()t=self.tt.pensize(3)t.speed(9)t.ondrag(getPo......
  • 皮卡丘
    #!/usr/bin/envpython#-*-coding:utf-8-*-fromturtleimport*'''绘制皮卡丘头部'''defface(x,y):"""画脸"""begin_fill()penup()#将海龟移动到指定的坐标goto(x,y)pendown()#......
  • 教你用Python画哆啦A梦、海绵宝宝、皮卡丘、史迪仔!
    一、哆啦A梦  由于代码过长,这里仅显示部分代码:fromturtleimport*importturtleastfromrandomimport*#五轨迹跳跃defmy_goto(x,y):penup()goto(x,y)pendown()defeyes():fillcolor('#ffffff')begin_fill()tracer(False)......
  • 教你用Python画个可爱的皮卡丘!(附完整源码)
    版权声明:原创不易,本文禁止抄袭、转载,侵权必究! 一、去吧!皮卡丘!使用turtle(海龟库)制作而成,感觉挺好玩的,哈哈@>_<@,效果如下: 由于源码过长,这里仅展示部分代码:from......
  • 「ZJOI2011」道馆之战
    「ZJOI2011」道馆之战给定一棵树,每一个节点\(2\)个房间,可以为薄冰或者障碍物,给定两个操作:修改某一个房间的两个区域,查询从\(u\)走到\(v\)最短可以经过多少薄冰(最终......
  • XSS(跨站脚本漏洞)——皮卡丘练习(小白随笔)
    环境准备:小皮(phpstudy)、皮卡丘(pikachu)、burpsuite、火狐浏览器概念理解:1、新建txt文档,后缀改为php<html> <body> :静态页面<?php>后端代码:在服务器先做计算,在再......