UVA10859 放置街灯 Placing Lampposts(树状DP)

UVA10859 放置街灯 Placing Lampposts(树状DP)

在这里插入图片描述
在这里插入图片描述
这道题有两种解决方法,因为原图保证无重边无环无自环, 所以原图一定是一颗树(或森林).,都是树状DP,但是实现的过程大同小异,先说第一种吧:
一:

1.在这道题中有两个因素在影响着转移,一个是放置灯的数量,一个是如果放的灯的数量相同的情况下被两盏灯同时照亮的边的条数,但是这两个条件有个优先级,就是灯的个数大于被边同时被两盏灯照亮的边,那么我们是不是可以这样想,如果多了一个灯变量值+1000,而如果是灯照亮了一条边就+1,那么我们在最后就可以把ans/1000就是灯的个数,把ans%1000就是被照亮一条边的个数,那么被两盏灯照亮的的边的个数就是 m - ans%1000,这样一下就能用一个变量来表示三个值,但是我们要注意,要是被一盏灯照亮的边的个数超过1000了呢,那么这样的答案就是错误的,所以我们要使(1000这个数)大于边的个数,那么就避免了这种情况,在这里M是<1000的,所以我们用1000也是可以的,保险起见我们用两千。
2.值表示完了我们开始想动态数组和动态转移方程:
动态数组:dp[i][j]:编号为i 的节点的父亲节点的状态时j(0是不亮,1是亮),可能很多同学比较迷惑为什么要用父亲节点的状态,刚开始我也是比较迷惑,但是后来在写记忆化搜索的时候理解了,首先我们发现一个点可以有亮和不亮两种状态,亮是一定符合的,但是如果让这个点不亮的情况是否存在就需要判断了,而判断的依据主要就是父亲节点的状态,如果a–b,a如果不亮了,那么b一定是需要亮的,因为如果b不亮在,那么a和b的这条边就没有被点亮,与题目要求不符合,所以这就是我们为什么要用到父亲节点的状态的原因,可能有同学就想根节点怎么办呢?所以我们要再加一个特判,如果是根节点,那么这个点亮和不亮都能继续搜索下去,所以综上我们就可以知道:

int DP(int node,int start,int fa){
//node 是当前节点,start是父节点的状态,fa是父节点的编号
	if(vis[node][start])return dp[node][start];//记忆化搜索,这个点之前搜索过
	vis[node][start] = 1;
	int ans = 2000;//因为亮灯是绝对可以的操作
	for(int i=0;i<map[node].size();i++){
		int a = map[node][i];
		if(a != fa)//因为只能继续向他的子节点进行递归搜索,因为如果不加这个判断条件的话就又找回去了,所以我们要避免这种情况 
			map[node][start] += DP(a,1,node);//下一个节点是a,因为父亲是node,node的状态时亮灯,所以参数是1;
	}
	/==在这里求完了之后我们要注意一点就是我们不要忘记了边被一盏灯照亮的情况也是需要综合考虑的!!!==
	if(fa != -1 && start == 0) ans++;
	/在这里我们把灯亮的情况找完了,然后我们就要判断这盏灯能不能不亮,而灯可以不亮的情况只有两种,一种是这个点是根节点,一种是父亲节点的灯是亮着的
	if(fa==-1 || start){
		int sum = 0;
		for(int i=0;i<map[node].size();i++){
			int a = map[node][i];
			if(a != fa){ /上面我们已经解释过了
				sum += DP(a,0,node);
			}
		}
		if(start) sum++; /同样要考虑被一盏灯照亮的边
		ans = min(ans,sum); /取最优
	}
	/这样我们就求出最优的ans了,因为ans的值是dp[node][start]的值,所以不要忘了dp[node][start] = ans;
	dp[node][start] = ans;
	return ans;
}

每次从根节点做完一次DP之后以该点作为根节点的树就全部遍历完了,但是有可能是一个森林,所以我们要进行遍历:

for(int i=0;i<n;i++){
	if(vis[i][0]==0){
		ans += DP(i,0,-1);//该点作为根节点进行遍历,ans加上这棵树的值
	}
}

输出结果就好了

	printf("%d %d %d\n",ans1/k,m-ans1%k,ans1%k);//k也就是我们前面提到的2000

完整代码如下:

#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
const int maxn = 5000 + 10;
vector<int>map[maxn];
int dp[maxn][2];
int vis[maxn][2];
int k = 2000;
int DP(int node,int start,int fa){	
	if(vis[node][start]) return dp[node][start];
	vis[node][start] = 1;
	int ans = k;
	for(int i=0;i<map[node].size();i++){
		if(map[node][i]!=fa) ans += DP(map[node][i],1,node);
	}
	if(!start && fa>=0) ans++;
	
	if(start || fa<0){
		int sum = 0;
		for(int i=0;i<map[node].size();i++){
			if(map[node][i]!=fa) sum += DP(map[node][i],0,node);
		}
		if(fa >= 0) sum++;
		ans = min(ans,sum);
	}
	dp[node][start] = ans;
	return ans;
}
int main(){
	int t;
	cin >> t;
	while(t--){
		int n,m;
		cin >> n >> m;
		for(int i=0;i<=n;i++) map[i].clear();
		memset(vis,0,sizeof(vis));
//		memset(dp,0,sizeof(dp));
		for(int i=0;i<m;i++){
			int a,b;
			cin >> a >> b;
			map[a].push_back(b);
			map[b].push_back(a);
		}
		int ans1 = 0;
		for(int i=0;i<n;i++){
			if(!vis[i][0]){
				ans1 += DP(i,0,-1);
			}
		}
		printf("%d %d %d\n",ans1/k,m-ans1%k,ans1%k);
	}
	return 0;
}

第二种做法:
很多同学觉得不太舒服,那么我们就把dp[i][j]作为这个点的亮还是不亮的信息,同理,亮是1,不亮是0;
我们在这里看代码进行分析:

void dfs(int node,int fa){ /该点的编号与该点父亲的编号,如果父亲是-1,那么这个点就是根节点
	vis[node] = 1; /这个点被访问过了,在后面的统计中说明这个点所在的树已经被统计过了
	dp[node][0] = 0; /这个点不亮
	dp[node][1] = k; /这个点亮,亮的话就要+2000,也就是+k
	for(int i=0;i<map[node].size();i++){
		if(map[node][i]!=fa){//不是父节点,第一种情况已经给出说明
			dfs(map[node][i],node);//继续往下进行搜索
			int a = map[node][i];
			dp[node][0] += dp[a][1] + 1; /z这个点不亮然而他的儿子节点亮,所以这个点是被一盏灯照亮的,所以把加上儿子节点亮的值再加一
			dp[node][1] += min(dp[a][0]+1,dp[a][1]); /这个点是亮的,所以他的儿子可以亮可以不亮,取最小的就好,如果他的儿子不亮,那么说明他与他儿子连接的这条边是只被一盏灯照亮的,所以+1}
	}
}
for(int i=0;i<n;i++){
	if(!vis[i]){//如果该点没有被访问过,说明这个点所在的树没有被访问过,那么就以这个点作为根进行dfs
		dfs(i,-1);
		ans += min(dp[i][0],dp[i][1]);//取这种情况下最小的那个
	}
}

完整代码如下:

#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
const int maxn = 1000+10;
vector<int>map[maxn];
int dp[maxn][2];
int vis[maxn];
int k = 2000;
void dfs(int node,int fa){
	vis[node] = 1;
	dp[node][0] = 0;
	dp[node][1] = k;
	for(int i=0;i<map[node].size();i++){
		if(map[node][i]!=fa){
			dfs(map[node][i],node);
			int a = map[node][i];
			dp[node][0] += dp[a][1] + 1;
			dp[node][1] += min(dp[a][0]+1,dp[a][1]);
		}
	}
}
int main(){
	int t;
	cin >> t;
	while(t--){
		int n,m;
		cin >> n >> m;
		for(int i=0;i<n;i++) map[i].clear();
		memset(dp,0,sizeof(dp));
		memset(vis,0,sizeof(vis));
		for(int i=0;i<m;i++){
			int a,b;
			cin >> a >> b;
			map[a].push_back(b);
			map[b].push_back(a);
		}
		int ans = 0;
		for(int i=0;i<n;i++){
			if(!vis[i]){
				dfs(i,-1);
				ans += min(dp[i][0],dp[i][1]);
			}
		}
		printf("%d %d %d\n",ans/k,m-ans%k,ans%k);
	}
	return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值