P1453 城市環路 題解
阿新 • • 發佈:2020-08-03
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; }