【题解】P8817 [CSP-S 2022] 假期计划(bfs,dfs)

【题解】P8817 [CSP-S 2022] 假期计划

此题作为 CSP-S 的 T1,可以说是相当有难度了。感觉 T1 和 T2 换了个位置。(雾)

我作为场外 VP 选手赛时此题只得了 95pts(洛谷民间数据),靠着自己乱搞 dfs+剪枝水过去了 19 个点。

这里记录一下这道题。


题目链接

题意概述

给定一张 \(n\) 个点 \(m\) 条边的无向无权图,点有点权。需要找四个不同的点 \(a,b,c,d\),使得 \(1-a-b-c-d-1\) 构成五段路径,且每段路径满足路径长度小于等于 \(k+1\)。现在要使得四个点权之和最大,求最大的点权和。

思路分析

这个 \(+1\) 看起来很难受,所以我们先将 \(k\) 直接 \(+1\)

为了方便说明这里自定义一些语言:

  • \(x\)\(y\) 的最短路径 \(\le k\) 我们就称 \(x\)\(y\) 可达。
  • 方便起见我们认为点 \(x\)\(x\) 本身不可达、

95pts 乱搞

先讲述一下我乱搞 95pts 的做法。

我当时看到这道题,,第一眼感觉正解应该想不到,然后就去想办法乱搞。

最暴力的做法是,对于四个点从 \(1\) 开始 dfs,考虑每个点应该选什么,暴力枚举每个点然后最后判断是否符合条件,并更新答案即可。

我们考虑如何优化。

首先由于每段路径的长度都 \(\le k\),所以显然假如我们当前在 \(x\),只有距离 \(x\) 最短路径 \(\le k\) 的点才能拓展。那么我们可以从每个点首先预处理出来下一步可以到达的点。

无权图上求最短路首先想到 bfs,这个显然可以枚举每个点然后预处理出来从每个点 \(i\) 到其它的点 \(j\) 的最短路 \(dis_{i,j}\)。单次 bfs 时间复杂度 \(O(n)\),总时间复杂度 \(O(n^2)\),这个数据范围是 \(2500\) 级别的,可以随便过。

然后预处理出来对于每个点 \(i\) 可达的点的集合 \(f_x\),然后每次更新下一个点的时候只在当前点的可达点集合中更新即可。

另外还可以预处理出从 \(1\) 开始走可达的点,把它们打上标记。然后搜完三个点之后就在 \(f_c\) 里面找看是否有打上标记的点,若打上标记则直接更新答案。

考虑到 dfs 一定难逃剪枝,我们可以记录一下当前已经选过的点的点权和 \(sum\) 和图中最大点权 \(mx\),当剩下未选的点数 \((4-now)\times mx\le 当前答案~ ans\),那就说明一定不可行,直接剪掉即可。

需要注意的一点是,为了保证四个点不同,我们必须定义一个 \(vis_i\) 数组表示 \(i\) 是否被选过,若被选过就不能再选了。

然后这样做完,虽然看起来时间复杂度疑似 \(O(n^4)\),但实际上我们在各种剪枝优化中剪掉了很多没用的情况,所以实际上可以跑得很快。洛谷民间数据 95pts,除了 T 掉的那个点之外其它最慢的点也只跑了 200ms 多。

正解

我们发现在刚刚的暴力过程中,我们只要确定了前三个点,然后再只需要在第三个点 \(c\) 可达的点里面找被标记的点(\(1\) 可达的点)即可。

那么这可以给我们思考正解带来启发:由于要权值和最大,当我们固定了 \(a,b,c\) 之后,我们直接在 \(c\) 可达的点中选择被标记的非 \(a,b\) 且权值最大的点即可。

同理,由于环的对称性,可以发现当我们确定了 \(b,c,d\) 后,可以贪心的选择权值最大的 \(a\)

将上述两点结合起来,给了我们一种思路:

我们只需要枚举 \(b,c\),然后再将 \(b,c\) 可达的点按照权值从大到小排序,那么 \(a\) 就是 \(b\) 可达的点里面,且被标记的点中,非 \(c,d\) 的权值最大的点,\(d\) 就是 \(c\) 可达的点里面,且被标记的点中,非 \(a,b\) 的权值最大的点。

那么我们怎么做呢?难道直接枚举 \(b,c\) 然后枚举 \(b\) 可达的点的集合和 \(c\) 可达的点的集合吗?

emmm……仔细算一算复杂度发现最坏情况下其实趋近于 \(O(n^4)\)……那你优化了个屁。(不是)

仔细思考这样一件事情:

我们对于 \(a\) 有以下几条限制:

  • \(b\) 可达的点中;
  • 在被标记的点中(即 \(1\) 可达的点中)
  • \(c,d\)
  • 权值最大。

实际上,我们可以把前两条放在一块来处理,即,我们通过 bfs 处理出来的集合 \(f_x\) 可以不单单是每个点可达的点集,还可以让它满足同时是 \(1\) 可达的点,只要在原来的代码上稍作改动即可。(之后见代码)

现在剩下最后两个条件。如果没有第三个条件。那么我们只需存 \(f_x\) 中最大的直接更新即可。那么考虑到第三个条件,举个例子:假如当前 \(f_x\) 中权值最大的点与 \(c\) 冲突,那就要考虑权值次大的点,假如这个次大的点又与 \(d\) 冲突那么就要考虑次次大点……所以实际上答案一定在 \(f_x\) 的前三大中,我们只需要预处理出来每个 \(f_x\) 中的前三大即可。

到这里其实大体思路已经梳理完了。现在考虑如何写。

如果我们直接分类讨论每个点会与哪个点冲突由于我们同时要考虑 \(a\)\(d\) 这两个点的冲突情况,所以这里的细节非常复杂和麻烦。其实我们只需要枚举每个 \(f_b\)\(f_c\) 的前三大然后直接更新答案即可。因为它只有最多 \(9\) 种情况,所以时间复杂度相当于 \(O(n^2)\) 带个 \(9\) 的常数。

代码实现

95pts 乱搞做法

//A
#include<cstdio>
#include<iostream>
#include<queue>
#include<cstring>
#include<algorithm>
#define int long long
using namespace std;
const int maxn=2505;
int a[maxn],dis[maxn][maxn],vis[maxn],book[maxn];
int ans=0;
int n,m,k,mx;

basic_string<int>edge[maxn],edge2[maxn];

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

void bfs(int x)
{
	queue<int>q;
	q.push(x);
	while(!q.empty())
	{
		int now=q.front();
		q.pop();
		for(int nxt:edge[now])
		{
			if(dis[x][nxt])continue;
			dis[x][nxt]=dis[x][now]+1;
			q.push(nxt);
		}
	}
}

void dfs(int x,int now,int sum)
{
	if(sum+(4-now)*mx<ans)return ;//剪枝
	if(now==3)
	{
		for(int y:edge2[x])
		{
			if(book[y]&&vis[y]==0)//如果这个点没选过且在 1 可达点集中则更新答案。
			{
				ans=max(ans,sum+a[y]);
			}
		}
		return ;
	}
	for(int y:edge2[x])
	{
		if(vis[y])continue;
		vis[y]++;
		dfs(y,now+1,sum+a[y]);
		vis[y]=0;
	}
}

signed main()
{
	n=read();m=read();k=read();k++;
	for(int i=2;i<=n;i++)a[i]=read(),mx=max(mx,a[i]);
	for(int i=1;i<=m;i++)
	{
		int u,v;
		u=read();v=read();
		edge[u]+=v;
		edge[v]+=u;
	}
	for(int i=1;i<=n;i++)
	{
		bfs(i);//对每个点 bfs 预处理出来 dis
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=2;j<=n;j++)
		{
			if(i==j)continue;
			if(dis[i][j]&&dis[i][j]<=k)
			{
				edge2[i]+=j;//这份代码中的 edge2[i] 表示的就是 i 的可达点的集合。
			}
		}
	}
	for(int v:edge2[1])book[v]++;//标记所有的 1 可达的点。
	dfs(1,0,0);
	cout<<ans<<'\n';
	return 0;
}

正解

#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<algorithm>
#include<cstring>
#define int long long
using namespace std;
const int maxn=2505;
int ok[maxn][maxn],dis[maxn][maxn],w[maxn];//ok[i][j] 表示 i,j 是否可达。
int n,m,k,ans;

vector<int>f[maxn];

basic_string<int>edge[maxn];

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

int cmp(int a,int b){return w[a]>w[b];}

void bfs(int x)
{
	queue<int>q;
	q.push(x);
	dis[x][x]=0;
	while(!q.empty())
	{
		int now=q.front();
		q.pop();
		if(now!=x)
		{
			ok[x][now]++;
			if(x!=1&&ok[1][now])//如果起点不为 1 且是 1 可达的点。
			{
				f[x].push_back(now);
				sort(f[x].begin(),f[x].end(),cmp);//按边权从大到小排序。
				if(f[x].size()>3)f[x].pop_back();//只保留前三大。
			}
		}
		if(dis[x][now]>=k)continue;//如果当前 dis 已经大于等于 k 就没必要再用,因为不可能更新。
		for(int nxt:edge[now])
		{
			if(dis[x][nxt]!=-1)continue;
			dis[x][nxt]=dis[x][now]+1;
			q.push(nxt);
		}
	}
}

signed main()
{
	memset(dis,-1,sizeof(dis));
	n=read();m=read();k=read();k++;
	for(int i=2;i<=n;i++)w[i]=read();
	for(int i=1;i<=m;i++)
	{
		int u,v;
		u=read();v=read();
		edge[u]+=v;
		edge[v]+=u;
	}
	for(int i=1;i<=n;i++)bfs(i);//这里的 bfs 处理的是 f[x]
    //暴力枚举每个 b,c,并对每对 b,c 枚举可能的 9 种答案。
	for(int b=2;b<=n;b++)
	{
		for(int c=2;c<=n;c++)
		{
			if(!ok[b][c])continue ;//b 必须可达 c。
			for(int a:f[b])
			{
				for(int d:f[c])
				{
					if(a!=c&&b!=d&&a!=d)//a 显然本身不可能与 b,d 不可能与 c 冲突。
					{
						ans=max(ans,w[a]+w[b]+w[c]+w[d]);//更新答案。
					}
				}
			}
		}
	}
	cout<<ans<<'\n';
	return 0;
}
  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值