1. 程式人生 > 實用技巧 >《逆向工程核心原理》——TLS回撥函式

《逆向工程核心原理》——TLS回撥函式

很久沒寫過部落格了今天來水一下


樹形\(DP\)

顧名思義,就是在樹上的dp。

樹形dp常分為兩種:選擇節點(節點染色)問題樹形依賴揹包問題

先來看一道普通的treedp:


洛谷P1352 沒有上司的舞會

題目描述

某大學有 \(n\) 個職員,編號為 \(1…n\)

他們之間有從屬關係,也就是說他們的關係就像一棵以校長為根的樹,父結點就是子結點的直接上司。

現在有個週年慶宴會,宴會每邀請來一個職員都會增加一定的快樂指數 \(r_i\)

但是呢,如果某個職員的直接上司來參加舞會了,那麼這個職員就無論如何也不肯來參加舞會了。

所以,請你程式設計計算,邀請哪些職員可以使快樂指數最大,求最大的快樂指數。

輸入格式

輸入的第一行是一個整數 \(n\)
\(2\) 行到第 \((n+1)\) 行, 每行一個整數,第 \((i+1)\) 的整數表示 \(i\) 號職員的快樂指數 \(r_i\)

\((n+2)\) 到第 \(2n\) 行,每行輸入一對整數 \(l,k\) 代表 \(k\)\(l\) 的直接上司。

輸出格式

輸出一行一個整數代表最大的快樂指數。

樣例

輸入

7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

輸出

5

\(1<n<6 \times 10^3,-128\leq r_i\leq 127,1\leq l,k \leq n\)

且給出關係一定是樹。


\(DP\)一般是線上性情況下進行,遇到這種樹形結構該怎麼辦呢?

想想當初學習樹時如何遍歷樹的. . . . . . 對了!DFS!

對一棵樹,從根節點出發DFS,就是從父節點向子節點方向訪問,回溯時候就是子節點向父節點訪問。

利用DFS序,我們把一個樹形結構拍平成了線性序列。於是\(DP\)就有了實現空間。

\(dp[x][0/1]\)代表第\(x\)個節點選 / 不選所能達到的最大快樂值。

對於第\(x\)個節點,設其子節點為\(y\)

  • 若選,則它的所有子節點都不能選,\(dp[x][1]=\sum dp[y][0]+poi[x]\)

  • 若不選,則子節點可選可不選,\(dp[x][0]=\sum max(dp[y][0],dp[y][1])\)

其中 \(poi[x]\) 表示第\(x\)個人的快樂值。

程式碼如下:

#include<bits/stdc++.h>
using namespace std;
int head[1000010];
int ver[1000010];
int nxt[1000010];
int tot=0;
int e[1000010];
int dg[100010];//用來統計每個點的出度
int dp[6005][2];
int n;

void dfs(int u)
{
	dp[u][0]=0;
	dp[u][1]=e[u];
	for(int i=head[u];i;i=nxt[i])//遍歷子節點
	{
		int v=ver[i];
		dfs(v);
		dp[u][0]+=max(dp[v][0],dp[v][1]);/*狀態轉移*/
		dp[u][1]+=dp[v][0];
	}
	return ;
}
void add(int x,int y)//鄰接表存邊
{
	ver[++tot]=y;
	nxt[tot]=head[x];
	head[x]=tot;
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>e[i];
	}
	for(int i=1;i<n;i++)
	{
		int u,v;
		cin>>u>>v;
		add(v,u);
		dg[u]++;//統計出度,方便找根
	}
	int root;
	for(int i=1;i<=n;i++)
	{
		if(!dg[i]) 
		{
			root=i;
			break;
		}
	}
	dfs(root);
	cout<<max(dp[root][0],dp[root][1]);//比較根節點放或不放哪個更優
        return 0;
}

於是我們知道了,對於樹形dp,最常用的是DFS來實現遍歷。

當然,視不同情況,我們還可以用BFS+遞推和拓補排序來做,這裡就不展示了。

想看走這邊→傳送門

這裡還有些額外的例題:

P2016 戰略遊戲

一本通1579 皇宮看守

P1122 最大子樹和


接下來就是第二大類樹形DP:樹形依賴揹包。

一般的依賴揹包我們或許已經接觸過了,比如金明的預算方案

裡面的依賴層數很少,就兩層,直接暴力列舉沒有問題

但當層數過多時,很明顯就要使用到所講的樹形DP了。

先來了解一個概念:泛化物品 (以下有關概念摘自:徐持衡《淺談幾類揹包問題》)


  • 泛化物品定義: 考慮一種物品,它沒有固定的費用和價值,而是其價值隨著分配給它的費用變化而變化

對於一個泛化物品,可以用一個一維陣列\(G_i\)表示其費用與價值的關係:當費用為\(i\)時,相對應的價值為\(G_i\)

  • 泛化物品的和:把兩個物品合在一起的運算,就是列舉費用分配給兩個物品,

滿足:

\(G_j=max(G1_{j-k},G2_{k})(0 \leq k \leq j \leq C)\)

時間複雜度為\(O(C^2)\)

  • 對於一組成樹形依賴關係的物品,我們可以將每個子樹都看作一個泛化物品,那麼一個子樹的泛化物品就是子樹根節點這件物品和它的子節點所在子樹的泛化物品的和。

聽起來有點魔幻,那就結合實際例題來看:P2014 [CTSC1997]選課

題目描述

在大學裡每個學生,為了達到一定的學分,必須從很多課程裡選擇一些課程來學習,在課程裡有些課程必須在某些課程之前學習,如高等數學總是在其它課程之前學習。現在有 \(N\) 門功課,每門課有個學分,每門課有一門或沒有直接先修課(若課程 \(a\) 是課程 \(b\) 的先修課即只有學完了課程 \(a\),才能學習課程 \(b\))。一個學生要從這些課程裡選擇 \(M\) 門課程學習,問他能獲得的最大學分是多少?

輸入格式

第一行有兩個整數\(N,M\)(\(1 \leq N \leq 300, 1\leq M\leq 300\)

接下來的\(N\)行,第\((i+1)\)行包含兩個整數\(k_i\)\(s_i\)\(k_i\)表示第\(i\)門課的直接先修課,\(s_i\)表示第\(i\)門課的學分,若\(k_i=0\)則表示沒有先修課。\((1 \leq k_i \leq N, 1 \leq s_i \leq 20)\)

輸出格式

只有一行,表示選\(M\)門課的最大學分。

樣例

輸入

7  4
2  2
0  1
0  4
2  1
7  1
7  6
2  2

輸出

13

非常明顯的樹形結構。

不妨設\(dp[i][j]\) 為對於節點\(i\)總共選\(j\)門課所能達到的最大學分。

很明顯我們要找到它的子節點的最大和,設其子節點為\(y\),我們可以列舉對於每個\(y\)分配的費用,取最大值處理。

狀態轉移方程為\(dp[i][j]=max(dp[i][j],dp[i][j-k+1]+dp[y][k]+poi[y]); (0 \leq k \leq j)\)

看著有點複雜是不是? 其實只要普通揹包問題熟練了,這個轉移方程完全可以自己推匯出來。

還是先放程式碼:

#include <bits/stdc++.h>
using namespace std;
int n,m;
int head[100010];
int ver[100010];
int nxt[100010];
int tot=0;
int poi[100010];
int dp[1010][1010];

void dfs(int x,int n)
{
	for(int i=head[x];i;i=nxt[i])
	{
		int y=ver[i];
		if(y==n) continue;
		dfs(y,x);
		for(int j=m;j>=0;j--)//倒著列舉,避免狀態更新重疊
		{
			for(int k=0;k<j;k++)//列舉給y節點分配的費用,或者說課數
			{
				dp[x][j]=max(dp[x][j],dp[y][k]+poi[y]+dp[x][j-k-1]);
			}
		}
	}
}
void add(int x,int y)
{
	ver[++tot]=y;
	nxt[tot]=head[x];
	head[x]=tot;
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int a,c;
		cin>>a>>c;
		add(i,a);
		add(a,i);
		poi[i]=c;
	}
	dfs(0,0);
	cout<<dp[0][m];
}

看完之後最好再看看題自己再試著推一下狀態轉移方程~~

附上一道樹形揹包的變種練習題:click me