Luogu P1902 刺杀大使

Luogu P1902 刺杀大使

是最近做到比较有心得的一道题。

我做了两种方法:

  • 最小生成树
  • 二分+DFS

最小生成树

虽然从题目标签上来看,二分+DFS才是这道题的正解。但事实上我是先写出最小生成树的做法,可能是因为这种做法的思路比较自然,而且复杂度没那么玄学吧。

建图

将第 $1$ 行看作一个点(记为起点),将第 $n$ 行看作一个点(记为终点),此两点点权为 $0$ 。第 $2$ 行到第 $n-1$ 行每行 $m$ 个点,点权按题目中对应位置赋值。

将起点与第 $2$ 行的每个点连一条边;类似的,将第 $n-1$ 行的每个点与终点连一条边。第 $2$ 行到第 $n-1$ 行则是按四连通的方式连边。

在以上连边过程中,显然所连的都是无向边;由题意,每条边的边权定义为该边两个端点的点权中较大的那一个。

附上一幅样例对应的图:

graph.png

算法

这里用类似最小生成树的算法(我用的是Kruskal,Prim应当也是可以的)。

注意到按最小生成树算法中,按边权从小到大的方式遍历加边,这一思路能够得到本题所求的路线。只需将退出条件改为当起点与终点连通时退出,并调整更新答案的方式。

计算复杂度,不难得到边数为 $n^2$ 的级别。再根据Kruskal的复杂度,可知总复杂度为 $O(n^2 \log n^2)$。考虑到这里的 $n \leq 10^3$,是能够通过的。

代码

代码实现上,注意给点编号的方式。

#include<bits/stdc++.h>
#define N 1010

int n,m,siz,ans;
int p[N][N],fa[N*N];

struct node {
	int u,v,w;
};

node edge[N*N*4];

namespace WalkerV {
	void Read() {
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;i++) {
			for(int j=1;j<=m;j++) {
				scanf("%d",&p[i][j]);
			}
		}
		return;
	}

	bool Compare(node x,node y) {
		return x.w<y.w;
	}

	int Find(int x) {
		return fa[x]==x?x:fa[x]=Find(fa[x]);
	}

	void Merge(int x,int y) {
		fa[(Find(y))]=Find(x);
		return;
	}

	void Kruskal() {
		int cnt=0;
		std::sort(edge+1,edge+siz+1,Compare);
		for(int i=1;i<=siz;i++) {
			if(Find(edge[i].u)!=Find(edge[i].v)) {
				ans=std::max(ans,edge[i].w);
				Merge(edge[i].u,edge[i].v);
				i==1?cnt+=2:cnt++;
				//printf("edge:%d %d %d\n",edge[i].u,edge[i].v,edge[i].w);
			}
			if(Find(1)==Find((n-2)*m+2)) {
				return;
			}
		}
		//printf("Fail\n");
		return;
	}

	void Solve() {
		for(int i=1;i<=m;i++) { //col1
			edge[++siz]=(node){1,i+1,p[2][i]};
		}
		for(int i=2;i<=n-1;i++) { //col2~n-1
			for(int j=1;j<=m;j++) {
				if(j!=m) { //right
					edge[++siz]=(node){(i-2)*m+j+1,(i-2)*m+j+2,std::max(p[i][j],p[i][j+1])};
				}
				if(i!=n-1) { //down
					edge[++siz]=(node){(i-2)*m+j+1,(i-1)*m+j+1,std::max(p[i][j],p[i+1][j])};
				}
			}
		}
		for(int i=1;i<=m;i++) { //coln
			edge[++siz]=(node){(n-3)*m+i+1,(n-2)*m+2,p[n-1][i]};
		}
		/*	
		for(int i=1;i<=siz;i++) {
			printf("%d %d %d\n",edge[i].u,edge[i].v,edge[i].w);
		}
		*/
		for(int i=1;i<=(n-2)*m+2;i++) {
			fa[i]=i;
		}
		Kruskal();
		return;
	}

	void Print() {
		printf("%d\n",ans);
		return;
	}
}

int main()
{
	WalkerV::Read();
	WalkerV::Solve();
	WalkerV::Print();
	return 0;
}

二分+DFS

思路

注意到题目中“最大值最小”的表述,这把思路引向了二分。

我们二分题目所求的最小伤害代价。搜索则是常规的四连通寻路,并在前方点点权大于当前二分的答案时折返。

代码

代码实现上,注意每次二分时清空 $\texttt{vis}$ 数组。

搜索(尤其是还带有这种比较神奇的剪枝)的复杂度是难以计算(玄学)的。注意到还有以下几个减小常数的方法(我都没有用):

  • 把二分的上限从理论上的 $p_{max}$(即 $1000$)调整为当前数据下的 $p_{max}$,这能减少几次DFS。
  • 每次DFS前不清空 $\texttt{vis}$ 数组,而是改为记录到达每个点的次数来进行判断。
#include<bits/stdc++.h>
#define N 1010
#define INF 0x7FFFFFFF
#define debug printf("OK\n");

int n,m,ans,mid;
int p[N][N],d[4][2]={{1,0},{-1,0},{0,1},{0,-1}};
bool vis[N][N];

namespace WalkerV {
	void Read() {
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;i++) {
			for(int j=1;j<=m;j++) {
				scanf("%d",&p[i][j]);
			}
		}
		return;
	}

	int DFS(int x,int y) {
		if(x==n) {
			return true;
		}
		vis[x][y]=true;
		for(int i=0;i<=3;i++) {
			int nx=x+d[i][0],ny=y+d[i][1];
			//printf("(%d,%d)->(%d,%d)\n",x,y,nx,ny);
			if(nx<1||nx>n||ny<1||ny>m||vis[nx][ny]||p[nx][ny]>mid) {
				continue;
			}
			if(DFS(nx,ny)) {
				return true;
			}
		}
		return false;
	}

	int LowerBound(int l,int r) {
		int ret=0;
		while(r-l>=2) {
			mid=(l+r)>>1;
			memset(vis,false,sizeof(vis));
			if(DFS(1,1)) {
				ret=mid;
				r=mid;
			}
			else {
				l=mid;
			}
			//printf("l:%d r:%d mid:%d\n",l,r,mid);
		}
		return ret;
	}

	void Solve() {
		ans=LowerBound(0,1001);
		return;
	}

	void Print() {
		printf("%d\n",ans);
		return;
	}
}

int main()
{
	WalkerV::Read();
	WalkerV::Solve();
	WalkerV::Print();
	return 0;
}

两种算法的比较

code.png

可见在这道题上搜索更快,而且空间也更小(毕竟是标签正解)。

posted @ 2021-08-09 12:32  WalkerV  阅读(95)  评论(0编辑  收藏  举报