HDU 4679 Terrorist’s destroy

9 篇文章 0 订阅
6 篇文章 0 订阅

可以树DP(可能不是很容易写),也可以利用树的直径的性质,这里用的是第二种方法,等会再去写第一种。

做这道题以前也遇到过类似的,一直是用树DP做,感觉不是很好写,容易写错,这题这题和同学讨论之后有了新发现,树直径的性质,百度之后发现也是对的,所以比较推荐这种解法,题意是给定一颗二叉树,你可以断其中一条边,断边需要消耗一定能量值,之后树变成两棵二叉树,对于每个树求他们的直径,取两个中的较大值乘以你断边消耗的能量就是我们要的值,你的任务就是是这个值最小!

树DP的话可以这样,我们先做一遍DFS,任取一点为根,DFS的时候保存每个点的子树的最大深度,然后第二遍DFS,树形DP经典的转移,这一遍的转移需要记录这个节点的子树深度最大值,次大值,以及他的父亲能传给他的深度最大值,这样就可以O(n)算出以每个节点作为根的时候的最大深度,问题基本解决,总之目测不好写。

但是利用树的直径会好做一些,对于和这题相似的情形:把一棵树断成两个树,求两个新树的直径,那么如果断的边在原树的直径上的话,新树的直径应该是,断处的那个节点DFS(不经过原树直径的边)下去找到的最大深度加上原树直径上的那一段,如图:

假设BAD是树的一条直径,那么如果在A处断了,左边这棵树的直径应该是从A,DFS下去找到的最大深度点设为C,和原树直径的端点B的距离之和,即BAC,证明并不难,留给看这篇结题报告的人,所以我们就有了解这类题目的新思路:先找树的直径,这是很经典的问题,做两次DFS,或者BFS,这里需要记录直径的顶点以及边的信息所以会比较麻烦一点,然后的工作就是枚举断的边是树上哪条边,有两种情况,如果没有断直径上的边,那么之后两棵树的最常规显然与原树相同,这种情况我们只需在一开始挑出能量权值最小的边来乘以直径就行,如果断的是直径上的边,这时候就是对断的那条边的两个断点做DFS,记录不在直径上的点到他的最远距离,然后加上直径上的一段就行了,对于后一种情况,我们没有办法再优化了,只能枚举,但是看看复杂度你会发现这样做是O(n)的,线性的为什么呢?因为我们对断点的那两个点做DFS并不会访问整棵树,而是只访问了与A相联通的不在直径上的点,所以每个节点只会被访问1次,所以是O(n)的,这样做的时候其实还是有很多细节iei要注意的,一开始没写对,调了好久,然后我的树是用邻接表实现的,所以用起来要与很多信息关联会很蛋疼,所以推荐其他做这题的人用STL的链表,把信息保存在一起会方便一些,详见代码,注释标出了要注意的地方,我也是在给自己加深印象!

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<string>
#include<memory>
#include<map>
#include<sstream>
#pragma warning (disable : 4996)
#define mem(a) memset(a, 0, sizeof(a))
#define sl(a) strlen(a)
#define LL long long
#define dou double
const int Mod = 1000000007;
const int N = 111111;
using namespace std;

int fi[N], en[2 *N], ne[2 * N], v[2 * N], tot, vis[N], q[N], d[N], tra[N], ee[N], rr[N], maxid, ma, dm;

struct po{
	int fa, v, go;
}f[N];

void add(int x, int y, int vv){
	ne[++tot] = fi[x], fi[x] = tot, en[tot] = y, v[tot] = vv; //邻接表,v维护的是能量信息
}

void bfs(int x){ //BFS一开始不想用DFS,因为怕爆栈,但后面还是用了,(因为下一步用DFS貌似会方便一些)
	int i, j, st, e;
	st = e = 0;
	q[st] = x, d[x] = 0, ma = maxid = -1; //ma记录最远距离,maid记录最远距离的点
	while (st <= e){
        vis[q[st]] = 1;//一定要记得标记是否被访问,否则死循环,已经不知道多少次忘记了!!!!
		for (int go = fi[q[st]]; go; go = ne[go]){ //注意这里go = fi[q[st]], 我老是会写成 go = q[x], BFS应该是队列的前面,DFS才是那样写的
			if (en[go] != q[st] && !vis[en[go]]){ //这里也是要注意的,和上一行一样的问题
				q[++e] = en[go]; //先++e,如果一开始e = 1,那么可以e++
				d[en[go]] = d[q[st]] + 1;//维护距离信息
				vis[en[go]] = 1;//一定不要忘记!!!
				maxid = ma < d[en[go]] ? en[go] : maxid;//维护最远点的ID
				ma = ma < d[en[go]] ? d[en[go]] : ma;
				f[en[go]].fa = q[st], f[en[go]].v = v[go], f[en[go]].go = go; //维护各种蛋疼信息,父节点,能量值,边的标号,所以不推荐像我这样笨的方法
			}
		}
		st++;//别忘了st++
	}
}

void dfs(int x, int p, int q, int dep){//DFS我们需要p,q是因为断的点我们不能访问直径,p记录断点的左边那个直径上的点,q右边那个
	for (int go = fi[x]; go; go = ne[go]){
		if (en[go] != p &&en[go] != q && !vis[en[go]]){ //这样,就不会访问p,q了,就不会访问到直径上面去
			dfs(en[go], x, x, dep + 1); //之后的DFS本来就不再直径上,所以不访问父节点就可以了,其实也可以用做标记来防止访问父节点,
													//但是这题我们的标记数组已经被用到其他地方去了,不想再开一个了
		}
	}
	dm = max(dm, dep); //维护找到的最大深度
}

int main(){
	long long re, n, m, i, j, k, t, x, y, z, mi = 11111, tem1, tem2, ans, ca = 1;
    int size = 256 << 20; // 256MB
    char *p = (char*)malloc(size) + size;
    __asm__("movl %0, %%esp\n" :: "r"(p)); //手工开栈,防止DFS爆栈,G++写法
	ios :: sync_with_stdio(false);
	cin >> t;
	while (t--){
		mi = 11111; //注意初始化,我WA过2次
		cin >> n; ans = -1;
		for (i= 0; i < n - 1; ++i) {
			cin >> x >> y >> z;
			add(x, y, z), add(y, x, z);
			ans = mi > z || ans < 0 ? i + 1 : ans; //边的编号与边的能量值有关,这种时候总是先维护标号id,再维护值,ans是答案,也就是我们要求的边的编号
			mi = min(mi, z); //后维护值
		}
		bfs(1); //第一遍广搜,找最远点
		mem(vis); //清空标记!!!否则下一步不会做任何事情,因为每个点都被标记了,不会再访问
		bfs(maxid); //第二遍广搜,记录直径信息了,其实这两次BFS是同一个,所以第一次也记录了一些假的直径信息,会有额外耗时,所以有些人愿意多谢一个BFS1
							//来省略这些额外不需要的步骤,不过是复制粘贴的事,我就不蛋疼再写一个了,反正这点时间也没卡我,最终代码还是AC了,虽然接近了3s的时限
		tem1 = ma * mi; //这是原树直径乘以能量最小边
		for (i = 0; i < ma; ++i) 
			tra[i] = maxid, rr[i] = f[maxid].go, ee[i] = f[maxid].v, maxid = f[maxid].fa; //这是记录直径的,tra是直径节点,ee是直径上的边的能量值,rr是边的编号,足见有多蛋疼
		tra[ma] = maxid;
		mem(vis); //清空标记,原因和上面一样,再一次提醒自己不要忘记
		for (i = 0; i < ma; ++i){
			dm = 0;
			dfs(tra[i], (i > 0 ? tra[i - 1] : -1), tra[i + 1], 0); //深搜左边tra[i - 1], tra[i + 1] 是当前要断的那条直径上的变得左端点的左右两个相邻节点,所以当节点时最左边时,他的左相邻节点不存在,可以设置为-1
			tem2 = ee[i] * (dm + i); //dm是全局的,记录的是找到的最大深度,所以下面要初始化为0,i显然是轨道上那一段直径的长度,所以这个方法还是有好处的,很好算
			dm = 0;
			dfs(tra[i + 1], tra[i], (i + 2 > ma ? -1 : tra[i + 2]), 0); //深搜右边,同理右断点的右相邻点可能不存在,设为-1
			tem2 = max(tem2, ee[i] * (dm + ma - i - 1)); //取两棵树直径最大值和断边的能量值相乘
			if (tem1 == tem2){
				ans = ans > (rr[i] + 1) / 2 ? (rr[i] + 1) / 2 : ans;//相等时候取编号小的边,由于是数组模拟邻接表,所以无向边被存了两次,编号需要加1除以2
			}
			else if (tem1 > tem2) ans = (rr[i] + 1) / 2; //小于直接更新答案
			tem1 = min(tem1, tem2);
		}
		cout << "Case #" << ca++ << ": " << ans << endl;
		mem(f), mem(q), tot = 0, mem(ne), mem(fi), mem(en), mem(vis), mem(rr), mem(ee);//不要忘记初始化,可能有几个不一定需要清空,但是以防万一,我还是全部清空了
	}

	return 0;
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值