1. 程式人生 > 實用技巧 >P1453 城市環路 題解

P1453 城市環路 題解

P1453 城市環路 題解

間隙

前置知識

  • 樹形dp,基環樹

大致題意

給一顆含有點權的基環外向樹

假如兩個點之間有一條邊連線,如果選擇了其中一端的節點,那另一段的節點則不可選擇

求:最大貢獻

分析

先講一下什麼是基環樹。

基環樹,簡單來說就是多了一條邊的樹,產生了一個環形結構,環上的每個節點都是一顆樹的根

畫成圖的話大概是這個樣子(基環外向樹)

一般來說,這種題目的做法都是先找到環,斷開環中的一條邊,
把它當成一般的樹形\(DP\)來做。

如何找環?

一般有\(dfs\)跟並查集兩種方法 , 這裡我採用的是並查集的做法

一開始每個節點都是一個獨立的集合

每連線一條邊,就把這兩個點合併到一個集合中

如果在連線一條邊之前,兩個節點就已經在一個集合中了,說明這兩個節點已經聯通了,再連線這條邊必然會產生環的情況

如何轉移?

找到了環之後,只需要將環上的這條邊斷開即可

這樣的話就可以當作普通的樹形\(DP\)來做了

\(f[i][0]\)為選第\(i\)個節點產生的最大貢獻

\(f[i][1]\)為不選第\(i\)個節點產生的最大貢獻

如果選了第\(i\)個節點,那它的兒子肯定都不能選

反之,兒子可以選擇選,也可以選擇不選

得到轉移方程:

\(f[u][0] = \sum f[v][0]\)

\(f[u][1] = \sum max(f[v][1],f[v][0])\)

程式碼實現

思路明白了程式碼實現應該就不難了

要注意的是環上的兩個點都可以作為樹的根節點,因此在\(DP\)的時候要把兩個點都跑一遍

具體的細節註釋有寫

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+10;
struct edge{//存圖 
	int v,next;
}e[MAXN<<1];
int f[MAXN][2],w[MAXN];//dp陣列,點權 
double k; 
int fa[MAXN];
int head[MAXN<<1],cnt = 0;
int root1,root2;//環上的兩個點 
int n;
void add(int u,int v){//前向星 
	e[++cnt].v = v;
	e[cnt].next = head[u];
	head[u] = cnt;
} 
int find(int x){//查詢集合 
	if(fa[x]==x){
		return x;
	}
	else{
		return fa[x] = find(fa[x]);
	}
}

void circle(int u,int fa){//樹形dp 
	f[u][1] = w[u],f[u][0] = 0;//初始化 
	for(int i=head[u];i;i=e[i].next){
		int v = e[i].v;
		if(v!=fa){ 
			circle(v,u);
			f[u][0]+=max(f[v][1],f[v][0]);//轉移 
			f[u][1]+=f[v][0];
		}
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&w[i]);
		fa[i] = i;//初始化集合 
	}
	for(int i=1;i<=n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		u++,v++;
		if(find(u)==find(v)){//如果在加邊前就在一個集合中了,說明找到了環 
        	root1 = u,root2 = v;//記錄環上的兩個點 
        	continue;//直接跳過加邊操作,相當於斷開這條邊 
		}
        add(u,v);
		add(v,u);
		fa[find(v)] = find(u);//合併集合 
	}
	scanf("%lf",&k);
	circle(root1,0);
	double r1 = f[root1][0];//選root1 
	
	circle(root2,0);
	double r2 = f[root2][0];//選root2 
	
	printf("%.1lf",max(r1,r2)*k);//取最大 
	return 0;
}