題目鏈接
如果它不是有條邊,而是條邊的話,它就是一棵樹,也就是沒有上司的舞會。
現在只多了一條邊,那它就是基環樹。相當於多了一個環,其他的結構沒有很大的變化。至於對於基環樹的處理,一般可以這麼處理:
- 找到環
- 將環斷開,讓它成爲一棵樹,對於斷開邊的兩個端點分別進行樹形dp。當然,需要注意一些關於這兩個點的限制條件
- 將信息整合,更新答案
需要注意的是,這裏可能不只是一棵樹,要對每個塊進行相同的處理並加入到答案中。還要注意數據範圍問題,需要開。
對於本題的一般樹形態,我們將狀態設計爲,表示對於以爲根節點的子樹,該節點不選/選的時候的點權和最大值。因爲和的兒子不能同時被選中,所以(爲的兒子),也即當前點的兒子不受限制;,也即你選了我一定不能選我的兒子。對於一棵樹的答案,就是
再來觀察一下題目中基環樹的形態,由於一個節點只會有一個仇恨的人,所以將我的仇人連向我(雖然成了仇人的兒子有點憋屈= =b)。這個時候,每個點有且僅有一個節點,建出來的一定是一棵外向樹。如下圖:
不會出現有兩條邊指向同一個點的情況:
所以我們在找環的時候,一路回退,一定能找到當前基環樹的環。
以上面那棵合法的舉個例子,比如我發現7號點沒有被訪問過,這說明這棵樹的答案我還沒有統計,從這個點開始往回到環上。過程如下:
當發現當前點的父親已被遍歷,說明當前點和它的父親都在這個環上,強行將這條邊斷開,分別將其作爲根節點進行處理。又因爲這兩個不能同時選,我們乾脆就在處理一個的時候,將另一個的設爲負無窮。這樣是不會影響最終答案的,是因爲我們分別遍歷的時候已經考慮了設爲根節點的點選或不選的情況了,可以避免同時被選的非法情況。每處理一次,就嘗試更新當前樹的答案,處理完後將兩種情況的最大值加入到總的答案中。
在向兒子節點遍歷之前,先設好邊界條件,也就是(當前點的點權)。記得你是要在一棵樹中跑兩次的,需要每次遍歷之前都設好邊界條件。
代碼:
#include <cstdio>
typedef long long ll;
const int maxn=1000000+10;
const ll INF=1LL<<60;
int head[maxn],to[maxn],nxt[maxn],fa[maxn];
int tot;
int root;
ll w[maxn],f[maxn][2];
ll ans;
bool vis[maxn];
ll max(ll x,ll y) {return x>y?x:y;}
void add(int u,int v)
{
nxt[++tot]=head[u];
head[u]=tot;
to[tot]=v;
}
void dfs(int u)
{
vis[u]=1;
f[u][0]=0,f[u][1]=w[u];
for (int i=head[u];i;i=nxt[i])
{
int v=to[i];
if (v!=root)
{
dfs(v);
f[u][0]+=max(f[v][0], f[v][1]);
f[u][1]+=f[v][0];
}
else
f[v][1]=-INF;
}
}
int FindCircle(int u)
{
vis[u]=1;
while(!vis[fa[u]])//因爲入邊唯一,這樣一直往前找方便
{
u=fa[u];
vis[u]=1;
}
root=u;
dfs(u);
ll tans=max(f[u][0], f[u][1]);
u=fa[u];
root=u;
dfs(u);
ans+=max(tans, max(f[u][0], f[u][1]));
}
int main()
{
int n;
scanf("%d",&n);
for (int v=1;v<=n;v++)
{
int u;
scanf("%lld%d",&w[v],&u);
add(u, v);
fa[v]=u;
}
for (int i=1;i<=n;i++)
if (!vis[i])
FindCircle(i);
printf("%lld\n",ans);
return 0;
}