疫情控制题 数据结构课设 二分+贪心+树上倍增+链式前向星。

疫情控制题 数据结构课设 二分+贪心+树上倍增+链式前向星。


系统完善且有详细代码注释哦!

题目

H 国有 n 个城市,这 n 个城市用 n-1 条双向道路相互连通构成一棵树,1 号城市是首都,
也是树中的根节点。
H 国的首都爆发了一种危害性极高的传染病。当局为了控制疫情,不让疫情扩散到边境城市
(叶子节点所表示的城市),决定动用军队在一些城市建立检查点,使得从首都到边境城市
的每一条路径上都至少有一个检查点,边境城市也可以建立检查点。但特别要注意的是,首
都是不能建立检查点的。
现在,在 H 国的一些城市中已经驻扎有军队,且一个城市可以驻扎多个军队。一支军队可
以在有道路连接的城市间移动,并在除首都以外的任意一个城市建立检查点,且只能在一个
城市建立检查点。一支军队经过一条道路从一个城市移动到另一个城市所需要的时间等于道
路的长度(单位:小时)。
请问最少需要多少个小时才能控制疫情。注意:不同的军队可以同时移动。

引言

二分+贪心+树上倍增+链式前向星。
这是一个非常烧脑的题目,网上查找了诸多文献,但没有一个系统完善的讲解,所以我整合之后发了出来。

思路

这道题用到了二分+贪心+树上倍增+链式前向星。

链式前向星

链式前向星是一种图的存储结构,至于这里为什么要用它,是因为邻接矩阵是好写但不好用,而邻接表是好用但不好写,链式前向星算是一种比较中庸的方式。
1,Next,表示与这个边起点相同的上一条边的编号。
2,head[ i ]数组,表示以 i 为起点的最后一条边的编号。

链式前向星的加边操作
void add_edge(int u, int v, int w)//加边,u起点,v终点,w边权
{
    edge[cnt].to = v; //终点
    edge[cnt].w = w; //权值
    edge[cnt].next = head[u];//以u为起点上一条边的编号,也就是与这个边起点相同的上一条边的编号
    head[u] = cnt++;//更新以u为起点上一条边的编号
}

这是链式前向星的访问遍历
for(int i=head[u];~i;i=edge[i].next)

超好懂得链式前向星传送门

二分法

二分法就是能够处理可以有序排列得问题,用于求最大化最小值,最小化最大值。在这里我们想,如果给定一个时间它可以让军队完成疫情控制,那么比它大的时间都可以,所以我们只需要缩短时间即可,即问题具有单调性,所以我们用二分法。
如果不考虑所给数据限制,这里我们二分的区间长度上限可以是军队从叶子节点到根节点走过最长距离再移动到根节点的儿子最长距离得和。能够包括所有情况。二分逐渐缩小区间直到找到最小值。

二分法的核心
while (l <= r) {//二分法
		mid = (l + r) >> 1;
		if (check(mid)) r = mid - 1, ans = mid, flag = 1;
		else l = mid + 1;
	}

贪心

对于每一个mid时间值,我们用贪心算法去考虑。每一个军队深度越小,则可以管理更多路径,在给定时间内,我们让每一个军队都往上跳,有两种情况:
(1) 如果军队可以达到根节点,则暂时停在根节点的子节点上,并记录其剩余时间。
(要注意到:这时我们实际上是把这些军队都移动到了根节点上,只是看上去先放在了根节点的子节点上而已,那么剩余时间是给定时间减去跳到根节点距离的差值,所以该子节点也是未被标记的即无军队驻扎,这里在代码中有解释。)
(2) 如果军达不到根节点,停在能走到的最大地方。停的节点做标记有军队驻扎。
此步骤用树上倍增法优化

树上倍增法

为什么用树上倍增法,因为在我们暴力解决中,向上寻找的时候只能一个一个跳,很慢,所以倍增法就可以一下跳好多个,跳过了就不跳,减少一半再继续跳,就很完美了。
树上倍增法:首先开一个n×logn的数组,比如fa[n][logn],其中fa[i][j]表示i节点的第2^j个父亲是谁。
然后,我们会发现有这么一个性质:
fa[i][j]=fa[fa[i][j-1]][j-1]
文字叙述为:i的第2^j个父亲 是i的第2^(j-1)个父亲的第
2^(j-1)个父亲。本来我们求i的第k个父亲的复杂度是O(k),现在复杂度变成了O(logk)。

这里用到倍增预处理
void bfs() {//倍增预处理 利用队列 有点类似广度搜索
	queue<int> q;
	q.push(1);
	dep[1] = 1;//根节点
	while (q.size()) 
	{
		int x = q.front(); q.pop();
		for (int i = head[x]; i; i = next1[i]) //依次取该节点的所有邻接点
		{
			int y = vlist[i];
			if (dep[y]) continue;//深度已有证明不是子节点 跳过
			dep[y] = dep[x] + 1; //否则对该点处理 深度加1
			f[y][0] = x;//它的邻接父亲
			dist[y][0] = edge[i];//到邻接父亲的距离
			for (int j = 1; j <= (int)(log(n) / log(2)) + 1; j++)//树上倍增法求它到上面所有父亲节点的距离或者说是倍增数组初始化
			{
				f[y][j] = f[f[y][j - 1]][j - 1];
				dist[y][j] = dist[y][j - 1] + dist[f[y][j - 1]][j - 1];
			}
			q.push(y);
		}
	}
	return;
}

树上倍增法的传送门
接下来我们继续考虑军队的移动问题
跳到根节点的军队又有两种情况。
1, 如果跳到根节点的军队它的剩余时间不够再原路跳回来,那它直接原地驻扎好了。
2, 剩余时间足够多的军队排序,需要封死的子节点到根距离排序,用双指针一一匹配。(这是贪心策略的核心)匹配完之后,如果所有需要封死城市均得到军队,那么疫情得到控制,否则无法控制。
(考虑这么贪心做的正确性,对于剩余时间不够的军队,如果选择跳过树根去另一个子节点s ′驻扎,则必然d i s t ( r o o t , s ) > d i s t ( r o o t , s ′ ) 这么做可能会导致需要一个剩余时间足够的军队s ′ ′从其本来位置跨过根跳至s,花费时间d i s t ( r o o t , s ′ ′ ) + d i s t ( r o o t , s ) 而这样做花的时间显然比s 原地驻扎,s ′ ′跨过根跳至s ′ 的情况长,于是贪心策略正确。)
总体思想就介绍完了。
重中之重呢就是二分中的check函数。

check函数

1,给定时间内用树上倍增法所有军队都往上跳。
2,用dfs查找需要封死的子树。
3,考虑有剩余时间的军队的两种情况。

bool check(ll mid) {
	memset(vis, 0, sizeof(vis));
	memset(tim, 0, sizeof(tim));
	memset(ned, 0, sizeof(ned));
	memset(h, 0, sizeof(h));
	memset(need, 0, sizeof(need));//数组初始置0
	numarmy2 = numcity = numarmy = 0;
	for (int i = 1; i <= m; i++) //对每一个军队都往上跳处理
	{
		ll x = army[i], cnt = 0;
		for (int j = (int)(log(n) / log(2)) + 1; j >= 0; j--)
			if (f[x][j] > 1 && cnt + dist[x][j] <= mid) //如果x的第2~j个父亲存在并且距离小于给定时间
				cnt += dist[x][j], x = f[x][j];//那么就往上跳  cnt记录军队跳的距离
		if (f[x][0] == 1 && cnt + dist[x][0] <= mid)//如果我们可以把这个军队提到根节点,
			h[++numarmy] = make_pair(mid - cnt - dist[x][0], x);//我们就暂时放在根节点的子节点上 并且该子节点是不标记的(注意:
		//这里我们将所有军队上移动,可以移动到根节点,只是看上去我们把它放在了子节点位置上,但这时该子节点没有军队驻扎,需要堪称需要封死的节点)
		//第一个参数是军队到根节点后剩余时间  第二个参数是军队现在停留的子节点位置
		else vis[x] = 1;//否则军队跳到能跳到的最大距离位置 并做标记该节点有军队
	}

	for (int i = head[1]; i; i = next1[i])//搜索树的各棵子树是否被封死了
		if (!dfs(vlist[i])) need[vlist[i]] = 1;//如果未封死 则将需要封死的子树根节点做标记

	sort(h + 1, h + numarmy + 1);//有剩余时间的军队排序
	for (int i = 1; i <= numarmy; i++)
	{
		if (need[h[i].second] && h[i].first < dist[h[i].second][0])
			need[h[i].second] = 0;//我们这个子树需要封,且剩余时间不够我们换一个分支了,我们就使用这个军队,
		//我们这个子树就被封死了,即我们的选择1  使用自己的军队
		else tim[++numarmy2] = h[i].first;//否则选择2 将有剩余时间的军队排序后存起来
	}

	for (int i = head[1]; i; i = next1[i])
		if (need[vlist[i]]) ned[++numcity] = dist[vlist[i]][0];//找出所有需要封死的子树的要花费的时间即到根节点的距离存起来

	if (numarmy2 < numcity) return 0;//如果剩余军队数量小于需要封死的城市,无法控制

	sort(tim + 1, tim + numarmy2 + 1), sort(ned + 1, ned + numcity + 1);//分别对军队和需要封死的节点排序 然后匹配
	int i = 1, j = 1;
	while (i <= numcity && j <= numarmy2)
	{
		if (tim[j] >= ned[i]) i++, j++;//军队可以移动过来封死
		else j++;//军队时间不足够移动 就选择时间更多的军队
	}
	if (i > numcity) return 1;//退出循环后 如果i大意味着需要封死城市全部被封死 可以控制
	return 0;
}

完整代码

完结撒花!

  • 10
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值