樹形DP二三知識

~hsm~發表於2019-03-05


1 之所以這樣命名樹規,是因為樹規的這一特殊性:沒有環,dfs是不會重複,而且具有明顯而又嚴格的層數關係。 利用這一特性,我們可以很清晰地根據題目寫出一個在樹(型結構)上的記憶化搜尋的程式。而深搜的特點,就是“不撞南牆不回頭”。這一點在之後的文章中會詳細的介紹。

首先是掃盲,介紹幾條名詞的專業解釋以顯示我的高階(大部分人可以略過,因為學習到樹規的人一下應該都懂……):

預備技能

動態規劃:

問題可以分解成若干相互聯絡的階段,在每一個階段都要做出決策,全部過程的決策是一個決策序列。要使整個活動的總體效果達到最優的問題,稱為多階段決策問題。動態規劃就是解決多階段決策最優化問題的一種思想方法。

階段:

將所給問題的過程,按時間或空間(樹歸中是空間,即層數)特徵分解成若干相互聯絡的階段,以便按次序去求每階段的解。

狀態:

各階段開始時的客觀條件叫做狀態。

決策:

當各段的狀態取定以後,就可以做出不同的決定,從而確定下一階段的狀態,這種決定稱為決策。 (即孩子節點和父親節點的關係)
  
策略:

由開始到終點的全過程中,由每段決策組成的決策序列稱為全過程策略,簡稱策略。

狀態轉移方程:

前一階段的終點就是後一階段的起點,前一階段的決策選擇匯出了後一階段的狀態,這種關係描述了由k階段到k+1階段(在樹中是孩子節點和父親節點)狀態的演變規律,稱為狀態轉移方程。

目標函式與最優化概念:

目標函式是衡量多階段決策過程優劣的準則。最優化概念是在一定條件下找到一個途徑,經過按題目具體性質所確定的運算以後,使全過程的總效益達到最優。

樹的特點與性質:

  1. 有n個點,n-1條邊的無向圖,任意兩頂點間可達
  2. 無向圖中任意兩個點間有且只有一條路
  3. 一個點至多有一個前趨,但可以有多個後繼
  4. 無向圖中沒有環;

廢話說完了,下面是正文:

拿到一道樹規題,我們有以下3個步驟需要執行:

  1. 判斷是否是一道樹規題:即判斷資料結構是否是一棵樹,然後是否符合動態規劃的要求。如果是,那麼執行以下步驟,如果不是,那麼換臺。
  2. 建樹:通過資料量和題目要求,選擇合適的樹的儲存方式。如果節點數小於5000,那麼我們可以用鄰接矩陣儲存,如果更大可以用鄰接表來儲存(注意邊要開到2*n,因為是雙向的。這是血與淚的教訓)。如果是二叉樹或者是需要多叉轉二叉,那麼我們可以用兩個一維陣列brother[],child[]來儲存(這一點下面會仔細數的)。
  3. 寫出樹規方程:通過觀察孩子和父親之間的關係建立方程。我們通常認為,樹規的寫法有兩種:
    a.根到葉子: 不過這種動態規劃在實際的問題中運用的不多。本文只有最後一題提到。
    b.葉子到根: 既根的子節點傳遞有用的資訊給根,完後根得出最優解的過程。這類的習題比較的多。

注意:這兩種寫法一般情況下是不能相互轉化的。但是有時可以同時使用具體往後看。

以下即將分析的題目的目錄及題目特點:

1、加分二叉樹:區間動規+樹的遍歷

2、二叉蘋果樹:二叉樹上的動規

3、戰略遊戲:多叉樹上的動規

4、最大利潤:多叉樹上的動規

5、選課:多叉樹轉二叉

6、選課(輸出方案):多叉轉二叉+記錄路徑

7、軟體安裝:判斷環+縮點+多叉轉二叉

【5、6、7屬於依賴問題的變形】

8、河流:多叉轉二叉+深搜

9、有線電視網:樹上的分組揹包

10、沒有上司的舞會:二叉樹上的動規

加分二叉樹

title

LUOGU 1040

描述 Description
  設一個n個節點的二叉樹tree的中序遍歷為(l,2,3,…,n),其中數字1,2,3,…,n為節點編號。每個節點都有一個分數(均為正整數),記第i個節點的分數為di,tree及它的每個子樹都有一個加分,任一棵子樹subtree(也包含tree本身)的加分計算方法如下:
  subtree的左子樹的加分× subtree的右子樹的加分+subtree的根的分數
  若某個子樹為空,規定其加分為1,葉子的加分就是葉節點本身的分數。不考慮它的空子樹。
  試求一棵符合中序遍歷為(1,2,3,…,n)且加分最高的二叉樹tree。要求輸出;
  (1)tree的最高加分
  (2)tree的前序遍歷
輸入格式 Input Format
  第1行:一個整數n(n<30),為節點個數。
  第2行:n個用空格隔開的整數,為每個節點的分數(分數<100)。
輸出格式 Output Format
  第1行:一個整數,為最高加分(結果不會超過4,000,000,000)。
  第2行:n個用空格隔開的整數,為該樹的前序遍歷。
樣例輸入 Sample Input
5
5 7 1 2 10
樣例輸出 Sample Output
145
3 1 2 4 5
時間限制 Time Limitation
每個測試點1s
來源 Source
NOIP2003第三題

analysis

看到這個問題,我們首先要看一看這是否是一個動態規劃,而我們看到若要使整棵樹的權值最大,則必須有左子樹和右子樹的權值最大,符合了最優化概念,所以這是一道動態規劃題目。

而卻不是一道樹規的題目。因為我們可以用區間動規的模型解決掉:直接定義一個f[i][j]f[i][j]表示從i到j的最大值,則

f[i][j]=max(f[i][k-1]*f[k+1][j]+a[k])

列舉kk即可。

好了,接下來就是如何建樹的問題了,畢竟這道題還披著樹形結構的外衣,不把樹建好的話,這道題是沒法子寫的。我們可以看到兩個名詞:二叉樹中序遍歷,再加上上面的區間DP,這樣的話,就可以利用二叉樹的性質把樹建成了。

至於前序遍歷,我們就用一個root[i][j]=kroot[i][j]=k表示iijj的根節點為kk即可。

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=31,inf=0x3f3f3f3f;
template<typename T>inline void read(T &x)
{
    x=0;
    T f=1,ch=getchar();
    while (!isdigit(ch) && ch^'-') ch=getchar();
    if (ch=='-') f=-1, ch=getchar();
    while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
    x*=f;
}
int f[maxn][maxn],root[maxn][maxn],point[maxn];
inline void get_front(int x,int y)
{
    if (root[x][y])
        printf("%d ",root[x][y]);
    if (root[x][ root[x][y]-1 ]) get_front(x,root[x][y]-1);
    if (root[ root[x][y]+1 ][y]) get_front(root[x][y]+1,y);
}
int main()
{
    int n;read(n);
    for (int i=0;i<=n;++i)
        for (int j=0;j<=n;++j)
            f[i][j]=1;
    for (int i=1;i<=n;++i)
    {
        read(point[i]);
        f[i][i]=point[i];
        root[i][i]=i;
    }
    for (int len=1;len<=n;++len)
        for (int i=1;i<=n;++i)
        {
            int j=i+len;
            if (j<=n)
            {
                int tmp=-inf;
                for (int k=i;k<=j;++k)
                    if (tmp<f[i][k-1]*f[k+1][j]+point[k])
                    {
                        tmp=f[i][k-1]*f[k+1][j]+point[k];
                        root[i][j]=k;
                    }
                f[i][j]=tmp;
            }
        }
    printf("%d\n",f[1][n]);
    get_front(1,n);
    return 0;
}

小結

本想著開始樹形DP的征程,卻沒想到第一題竟然是個披著樹形外衣的非樹形DP題,可見正確的演算法是事半功倍的演算法,同時,也告知我,必須對知識十分熟悉,只有這樣,才能從題目中剖析出模型,找到正確的演算法。

前方高能,真正的樹規將要出動!

蘋果二叉樹

title

LUOGU 2015

描述 Description
有一棵蘋果樹,如果樹枝有分叉,一定是分2叉(就是說沒有隻有1個兒子的結點)。這棵樹共有N個結點(葉子點或者樹枝分叉點),編號為1-N,樹根編號一定是1。
我們用一根樹枝兩端連線的結點的編號來描述一根樹枝的位置。下面是一顆有4個樹枝的樹:

2   5 
 \ / 
  3   4 
   \ / 
    1 

現在這顆樹枝條太多了,需要剪枝。但是一些樹枝上長有蘋果。
給定需要保留的樹枝數量,求出最多能留住多少蘋果。
輸入格式 Input Format
第1行2個數,N和Q(1<=Q<= N,1<N<=100)。
N表示樹的結點數,Q表示要保留的樹枝數量。接下來N-1行描述樹枝的資訊。
每行3個整數,前兩個是它連線的結點的編號。第3個數是這根樹枝上蘋果的數量。
每根樹枝上的蘋果不超過30000個。
輸出格式 Output Format
一個數,最多能留住的蘋果的數量。
樣例輸入 Sample Input
5 2
1 3 1
1 4 10
2 3 20
3 5 20
樣例輸出 Sample Output
21
時間限制 Time Limitation
1s

analysis

好啦,這道題毫無疑問就是一道樹形DP的題目,==父節點和子節點存在著相互關聯的階段關係。==嗯,這是第一步,判斷這是什麼模型。

第二步,面對樹形DP,我們的儲存方式有兩種:鄰接矩陣和鄰接表,面對資料量不大的題目,是可以使用鄰接矩陣的(例如本題,然而我沒用qwq)。

隨便你用什麼儲存方式,反正下面一步總是要考慮狀態轉移方程該怎麼寫。
我們用x表示當前節點,y為他的一顆子節點,siz[]siz[]表示以i為根節點樹上的邊數。
那麼,狀態轉移方程就可以這樣寫了:

f[x][j]=max(f[x][j],f[x][j−k−1]+f[y][k]+edge[i]) ( 1≤j≤min(q,siz[x]) , 0≤k≤min(siz[y],j−1) )

看到方程都很容易理解這其中的含義,但是後面的範圍是怎麼回事?

首先先看kkkk在此處表示取y子樹上的邊數,最大自然不能超過siz[y]siz[y]
但是為什麼要小於等於j1j-1而不是jj呢?

我們前面提到過了,題目中是有隱含條件的,若要選取子樹yy上的邊,則必須選取xxyy相連的邊,保證選取的邊全部與根節點相連。所以k就是以y節點為根的子樹保留的邊數。

然後就是jjsiz[x]siz[x]陣列是在實時發生變化的,它不斷加上自己子樹的邊,保證揹包容量不斷擴大,此處千萬不要寫成siz[y]siz[y]

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+10;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch) && ch^'-') ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
int ver[maxn],edge[maxn],Next[maxn],head[maxn],len;
inline void add(int x,int y,int z)
{
	ver[++len]=y,edge[len]=z,Next[len]=head[x],head[x]=len;
}
int n,q,siz[maxn],f[maxn][maxn];
inline void dfs(int x,int fa)
{
	for (int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (y==fa) continue;
		dfs(y,x);
		siz[x]+=siz[y]+1;
		for (int j=min(siz[x],q);j;--j)
			for (int k=min(siz[y],j-1);k>=0;--k)
				f[x][j]=max(f[x][j],f[x][j-k-1]+f[y][k]+edge[i]);
	}
}
int main()
{
	read(n);read(q);
	for (int i=1;i<n;++i)
	{
		int x,y,z;read(x);read(y);read(z);
		add(x,y,z),add(y,x,z);
	}
	dfs(1,-1);
	printf("%d\n",f[1][q]);
	return 0;
}

戰略遊戲 && 最大利潤

title

LUOGU 2016 戰略遊戲

描述 Description
century喜歡玩電腦遊戲,特別是戰略遊戲。但是他經常無法找到快速玩過遊戲的辦法。現在他有個問題。他要建立一個古城堡,城堡中的路形成一棵樹。他要在這棵樹的結點上放置最少數目的士兵,使得這些士兵能瞭望到所有的路。注意,某個士兵在一個結點上時,與該結點相連的所有邊將都可以被瞭望到。
請你編一程式,給定一樹,幫century計算出他需要放置最少的士兵。
輸入格式 Input Format
輸入檔案中資料表示一棵樹,描述如下:
第一行 N,表示樹中結點的數目。
第二行至第N+1行,每行描述每個結點資訊,依次為:該結點標號i,k(後面有k條邊與結點I相連),接下來k個數,分別是每條邊的另一個結點標號r1,r2,…,rk。
對於一個n(0<n<=1500)個結點的樹,結點標號在0到n-1之間,在輸入檔案中每條邊只出現一次。
輸出格式 Output Format
輸出檔案僅包含一個數,為所求的最少的士兵數目。
樣例輸入 Sample Input
輸入樣例1:
4
0 1 1
1 2 2 3
2 0
3 0
輸入樣例2:
5
3 3 1 4 2
1 1 0
2 0
0 0
4 0
樣例輸出 Sample Output
輸出樣例1:
1
輸出樣例2:
2
時間限制 Time Limitation
1s
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
上為戰略遊戲,下為最大利潤
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
描述 Description
政府邀請了你在火車站開飯店,但不允許同時在兩個相連線的火車站開。任意兩個火車站有且只有一條路徑,每個火車站最多有50個和它相連線的火車站。
告訴你每個火車站的利潤,問你可以獲得的最大利潤為多少。
例如下圖是火車站網路:
::點選圖片在新視窗中開啟::
最佳投資方案是在1,2,5,6這4個火車站開飯店可以獲得利潤為90
輸入格式 Input Format
第一行輸入整數N(<=100000),表示有N個火車站,分別用1,2。。。,N來編號。接下來N行,每行一個整數表示每個站點的利潤,接下來N-1行描述火車站網路,每行兩個整數,表示相連線的兩個站點。
輸出格式 Output Format
輸出一個整數表示可以獲得的最大利潤。
樣例輸入 Sample Input
6
10
20
25
40
30
30
4 5
1 3
3 4
2 3
6 4
樣例輸出 Sample Output
90
時間限制 Time Limitation
1s

analysis

我把兩道題放在了一起,就很明顯的想說明這兩道題是一樣的,只不過一個是取最小值,另一個是最大值而已。

這裡借鑑一下歐陽學長對於最大利潤一題的分析:

按照上一題的步驟,我們再來分析一遍:一、是否是動態規劃。這時可能很多人已經吐槽了:閉著眼都知道是動態規劃,不然你粘出來幹什麼??呵呵,沒錯,確實是。但是為什麼是呢??首先,這是棵樹,是一棵多叉樹。其次,當我們嘗試著把他向動態規劃上靠時,我們發現當前節點只與其孩子節點的孩子節點(這裡沒打錯,因為隔一個火車站)有關係。所以綜上所述,是動規,還是一個樹規,一個不折不扣的樹規!

接下來,第二步建樹。看範圍和題目發現,這是一個有著n&lt;100000n(&lt;100000)的多叉樹,所以只能用鄰接表儲存了。沒有根,我們一般通常指定1為根。

第三步:F[i]F[i]表示i這條根要,G[i]G[i]表示不要(也可以用f[i][1,0]f[i][1,0]來表示)。
然後以此列舉ii的孩子:如果ii要了那麼i的孩子就不能要,如果ii不要ii的孩子就可要可不要(取最大值)即可。
最後輸出maxf[1],g[1];max(f[1],g[1]);

戰略遊戲是一樣的。區別就是上面所說的。

code

戰略遊戲

#include<bits/stdc++.h>
using namespace std;
const int maxn=15e2+10;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch) && ch^'-') ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
int ver[maxn<<1],Next[maxn<<1],head[maxn],len;
inline void add(int x,int y)
{
	ver[++len]=y,Next[len]=head[x],head[x]=len;
}
int f[maxn],g[maxn];
inline void dfs(int x,int fa)
{
	g[x]=1;
	for (int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (y==fa) continue;
		dfs(y,x);
		f[x]+=g[y];
		g[x]+=min(f[y],g[y]);
	}
}
int main()
{
	int n;read(n);
	for (int i=1;i<=n;++i)
	{
		int x,k;
		read(x);read(k);
		for (int i=1;i<=k;++i)
		{
			int y;read(y);
			add(x,y);add(y,x);
		}
	}
	dfs(0,-1);
	printf("%d\n",min(f[0],g[0]));
	return 0;
}

最大利潤

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch) && ch^'-') ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
int ver[maxn<<1],Next[maxn<<1],head[maxn],len;
inline void add(int x,int y)
{
	ver[++len]=y,Next[len]=head[x],head[x]=len;
}
int f[maxn],g[maxn],a[maxn];
inline void dfs(int x,int fa)
{
	for (int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (y==fa) continue;
		dfs(y,x);
		f[x]+=g[y];
		g[x]+=max(f[y],g[y]);
	}
	f[x]+=a[x];
}
int main()
{
	int n;
	read(n);
	for (int i=1;i<=n;++i)
		read(a[i]);
	for (int i=1;i<n;++i)
	{
		int x,y;
		read(x);read(y);
		add(x,y);add(y,x);
	}
	dfs(1,1);
	printf("%d\n",max(f[1],g[1]));
	return 0;
}

選課

title

LUOGU 2014
CH 5402

題目描述
在大學裡每個學生,為了達到一定的學分,必須從很多課程裡選擇一些課程來學習,在課程裡有些課程必須在某些課程之前學習,如高等數學總是在其它課程之前學習。現在有N門功課,每門課有個學分,每門課有一門或沒有直接先修課(若課程a是課程b的先修課即只有學完了課程a,才能學習課程b)。一個學生要從這些課程裡選擇M門課程學習,問他能獲得的最大學分是多少?
輸入輸出格式
輸入格式:
第一行有兩個整數N,M用空格隔開。(1<=N<=300,1<=M<=300)
接下來的N行,第I+1行包含兩個整數ki和si, ki表示第I門課的直接先修課,si表示第I門課的學分。若ki=0表示沒有直接先修課(1<=ki<=N, 1<=si<=20)。
輸出格式:
只有一行,選M門課程的最大得分。
輸入輸出樣例
輸入樣例
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
輸出樣例
13

analysis

這同樣也是歐陽學長的分析,借過來用一下。(〃'▽ '〃)
繼續照著三步的方法判斷:一,題目大致一看,有點像有依賴的揹包問題,於是你扭頭就走,關掉了我的《樹規》,開啟了崔神犇的《揹包九講》。然後你哭了,因為有依賴的揹包問題只限定於一個物品只依賴於一個物品,而沒有間接的依賴關係。有依賴的揹包問題的模型,根本解決不了。崔神告訴你,這屬於樹規的問題,不屬於他揹包的範圍了。好了,回過來,我們接著分析。發現這是一棵樹,還是一棵多叉樹,嗯,很好,確定是樹規了。

然後第二步,建樹,一看資料範圍鄰接矩陣;

第三步動規方程:
f[i][j]ijf[i][j]表示以i為節點的根的選j門課的最大值,然後有兩種情況:
ii0i不修,則i的孩子一定不修,所以為0;
iij1ii修,則i的孩子們可修可不修(在這裡其實可以將其轉化為將j-1個對i的孩子們進行資源分配的問題,也屬於揹包問題);
f[1][m]滿答案是f[1][m]。問題圓滿解決,一氣呵成。

但……

身為追求完美的苦*程式猿的我們,不可以將它更簡單一點呢?

多叉轉二叉。

因為之前我們說過“在樹的儲存結構上,我們一般選的都是二叉樹,因為二叉樹可以用靜態陣列來儲存,並且狀態轉移也很好寫(根節點只和左子節點和右子節點有關係)。”所以轉換成二叉樹無疑是一種不錯的選擇。

我們開兩個一維陣列,b[i] (brother)c[i] (child) 分別表示節點i的孩子和兄弟,以左孩子和右兄弟的二叉樹的形式儲存這樣,根節點之和兩個節點有關係了,狀態轉移的關係少了,程式碼自然也就好寫了。

我們依舊f[i][j]表示以i為節點的根的選j門課的最大值,那麼兩種情況:
1.根節點不選修則

f[i][j]=f[b[i]][j];

2.根節點選修

f[i][j]=f[c[i]][k]+f[b[i]][j-k-1]+a[i](k表示左孩子學了k種課程);

取二者的最大值即可。

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=310;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch) && ch^'-') ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
int f[maxn][maxn],bro[maxn],chi[maxn],sub[maxn];
inline void dfs(int x,int y)
{
	if (f[x][y]>=0) return ;
	if (!x || !y)
	{
		f[x][y]=0;
		return ;
	}
	else dfs(bro[x],y);
	for (int i=1;i<=y;++i)
	{
		dfs(bro[x],i-1);dfs(chi[x],y-i);
		f[x][y]=max(f[x][y],max(f[ bro[x] ][y],f[ bro[x] ][i-1]+f[ chi[x] ][y-i]+sub[x]));
	}
	return ;
}
int main()
{
	int n,m;
	read(n);read(m);
	memset(f,-1,sizeof(f));
	for (int i=1;i<=n;++i)
	{
		int x,y;
		read(x);read(y);
		sub[i]=y;
		bro[i]=chi[x];
		chi[x]=i;
	}
	dfs(chi[0],m);
	printf("%d\n",f[chi[0]][m]);
	return 0;
}

等等,如果讓你輸出選課方案,你該怎麼辦?

選課(輸出方案)

title

題目和上面完全一樣,只是多讓輸出了個方案。

analysis

所以這道題目重點是考察的是樹的路徑記錄的問題。
  既然樹是遞迴定義的,所以我們依舊使用遞迴的形式來記錄路徑:使用一個bool陣列ans來進行遞迴,分兩種情況:取(1)和不取(0)。
  然後,我們繼續利用已經求得的f[i][j]f[i][j]的值來思考如何找到路徑:
  首先定義一個path()函式。如果f[i][j]=f[b[i]][j]f[i][j]=f[b[i]][j],那麼節點i必然沒有取,讓ans[i]=0;
  否則,節點i一定取到了。(為什麼呢?其實,這是依照第一問的dfs來思考的,第一問的dfs是這樣定義的,所以我們就這樣考慮了。)
  然後依照上一問,if(f[x][y]==f[b[x]][k1]+f[c[x]][yk]+s[x])if (f[x][y]==f[b[x]][k-1]+f[c[x]][y-k]+s[x]),那麼我們在i節點後選的一定是以上的方案,在這時讓ans[i]=1ans[i]=1,繼續深搜path()path()即可。最後從1到n依次輸出取到的點即可。

  其實,就是把我們樹規求解最大值的過程給反過來了。

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=520;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch) && ch^'-') ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
int f[maxn][maxn],bro[maxn],chi[maxn],sub[maxn];
inline void dfs(int x,int y)
{
	if (f[x][y]>=0) return ;
	if (!x || !y)
	{
		f[x][y]=0;
		return ;
	}
	else dfs(bro[x],y);
	for (int i=1;i<=y;++i)
	{
		dfs(bro[x],i-1);
		dfs(chi[x],y-i);
		f[x][y]=max(f[x][y],max(f[ bro[x] ][y],f[ bro[x] ][i-1]+f[ chi[x] ][y-i]+sub[x]));
	}
	return ;
}
bool ans[maxn];
inline void path(int x,int y)
{
	if (!x || !y) return ;
	if (f[x][y]==f[bro[x]][y]) path(bro[x],y);
	else
	{
		for (int i=1;i<=y;++i)
			if (f[x][y]==f[ bro[x] ][i-1]+f[ chi[x] ][y-i]+sub[x])
			{
				path(bro[x],i-1);
				path(chi[x],y-i);
				ans[x]=1;
				return ;
			}
	}
}
int main()
{
	int n,m;
	read(n);read(m);
	memset(f,-1,sizeof(f));
	for (int i=1;i<=n;++i)
	{
		int x,y;
		read(x);read(y);
		sub[i]=y;
		bro[i]=chi[x];
		chi[x]=i;
	}
	dfs(chi[0],m);
	printf("%d\n",f[chi[0]][m]);

	path(chi[0],m);
	for (int i=1;i<=n;++i)
		if (ans[i])
			printf("%d\n",i);

	return 0;
}

最後一道巨難的省選級別題目要上啦!

[HAOI2010]軟體安裝

title

LUOGU 2515

描述 Description
現在我們的手頭有N個軟體,對於一個軟體i,它要佔用Wi的磁碟空間,它的價值為Vi。我們希望從中選擇一些軟體安裝到一臺磁碟容量為M的計算機上,使得這些軟體的價值儘可能大(即Vi的和最大)。
但是現在有個問題:軟體之間存在依賴關係,即軟體i只有在安裝了軟體j(包括軟體j的直接或間接依賴)的情況下才能正確工作(軟體i依賴軟體j)。幸運的是,一個軟體最多依賴另外一個軟體。如果一個軟體不能正常工作,那麼他能夠發揮的作用為0。
我們現在知道了軟體之間的依賴關係:軟體i依賴Di。現在請你設計出一種方案,安裝價值儘量大的軟體。一個軟體只能被安裝一次,如果一個軟體沒有依賴則Di=0,這是隻要這個軟體安裝了,它就能正常工作。
輸入格式 Input Format
第1行:N,M (0<=N<=100,0<=M<=500)
第2行:W1,W2, … Wi, … ,Wn
第3行:V1,V2, … Vi, … ,Vn
第4行:D1,D2, … Di, … ,Dn
輸出格式 Output Format
一個整數,代表最大價值。
樣例輸入 Sample Input
3 10
5 5 6
2 3 4
0 1 1
樣例輸出 Sample Output
5
時間限制 Time Limitation
1s

analysis

這道題確實和第四道題很像,做完後翻討論區,有大佬就直接說:這不就是個tarjan縮點選課嗎?

但是,身為一名蒟蒻,tarjan是不可能會打的,所以學習了歐陽學長的寫法,用FloydFloyd判環縮點。
好啦,下面完整的將歐陽學長的分析放在這裡:

同樣,這道題目類似與第4題,是一個依賴的問題,毫無疑問是一道動態規劃,但是它確實是樹規麼?我們來想這樣一組資料,1依賴2,2依賴3,3依賴1。這樣符合題目要求,但有形成了環,所以不是一棵樹了。但是根據題目,這樣特殊的情況,要麼全要,要麼全就不要。所以,事實上我們可以將這個環看成一個點再來動規,即縮點。如何判斷是否是一個環呢,依照資料範圍,我們想到了floyed(弗洛裡德),這是在這種資料範圍內價效比最高的方式。最後樹規。於是一個比較清晰的步驟就出來了:判環,縮點,樹規。

接下來是細節:首先存樹,毫無疑問,是鄰接矩陣。

做floyed。如果兩點之間mapp[i][j]中有另一條路徑相連,即mapp[i][k]=1 && mapp[k][j]=1(1表示兩點是通的);那麼mapp[i][j]也是通的且是環。

縮點。這個是最麻煩的,麻煩在於我們要把縮的點當成一個新點來判斷,而且要判斷某個點是否在某個環裡。我們用染色法來判斷,用所佔的空間w控制顏色的對應,有以下三種情況:1、點i所在的環之前沒有判斷過,是新環。那麼,我們將這個新環放到陣列最後,即新加一個點,然後讓這兩個點的空間標記為負值tmpw,且tmpw+tmpn(新點的下標)等於原來的點數,這樣,我們就可以通過某個點的空間迅速找到他所在的新點。像鑰匙一樣一一對應;2、點i所在的環之前已經判斷過了,是舊環(已合成新點),且i是環的一部分。那麼我們就把i也加到這個新點裡面,即體積,價值相加即可;3、點j所在的環是舊環,但是i不是環的一部分(例如1依賴2,2依賴3,3依賴1。4也依賴1,那麼,4所在的是個環,但4不屬於環的一部分)。那麼,把j的父親轉到新點上d[j]= n-w[d[j]]。

以上縮點的工作做完之後,剩下的就是一棵樹。就可以在這上面動規了:先將其轉換成一棵左孩子右兄弟的二叉樹,之後記憶化。i的孩子不取f[b[x]][k]=dfs(b[x],k);f[b[x]][k]=dfs(b[x],k);
還是取:f[c[x]][yi]=dfs(c[x],yi);f[c[x]][y-i]=dfs(c[x],y-i);
f[b[x][i]=dfs(b[x],i);f[b[x][i]=dfs(b[x],i);
f[x][k]=max(f[x][k],v[x]+f[c[x]][yi]+f[b[x]][i]);f[x][k]=max(f[x][k],v[x]+f[c[x]][y-i]+f[b[x]][i]);

最後答案是f[c[0]][m]f[c[0]][m]

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=505;
template<typename T>inline void read(T &x)
{
    x=0;
    T f=1,ch=getchar();
    while (!isdigit(ch) && ch^'-') ch=getchar();
    if (ch=='-') f=-1, ch=getchar();
    while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
    x*=f;
}
int n,m,tmpn,tmpw;//tmpn 縮點之後的點總數
int w[maxn],v[maxn],d[maxn];
bool a[maxn][maxn];
inline void floyd()
{
	for (int k=1;k<=n;++k)
        for (int i=1;i<=n;++i)
            for (int j=1;j<=n;++j)
                a[i][j]|=a[i][k]&a[k][j];
}
inline void merge()
{
    tmpn=n;
    for (int i=1;i<=tmpn;++i)
        for (int j=1;j<=tmpn;++j)
        {
            if (a[i][j]==1 && a[j][i]==1 && i!=j && w[i]>0 && w[j]>0)//如果是新環;
            {
                ++tmpn;
                v[tmpn]=v[i]+v[j];
                w[tmpn]=w[i]+w[j];
                --tmpw;
				w[i]=w[j]=tmpw;
            }
            if (w[d[j]]<0 && w[j]>0 && a[j][d[j]]==1 && a[d[j]][j]==1)
            {//如果j依賴的點被合併(是舊環),且j在環裡
                w[n-w[d[j]]]+=w[j];
                v[n-w[d[j]]]+=v[j];
                w[j]=w[d[j]];
            }
            if (w[d[j]]<0 && w[j]>0)//如果j依賴的點在環裡,但是j不在環裡
                if ( (a[j][d[j]]==1 && !a[d[j]][j]) || (!a[j][d[j]] && a[d[j]][j]==1) )
                    d[j]=n-w[d[j]];
        }
}
int f[maxn][maxn*5],b[maxn],c[maxn];
inline int  dfs(int x,int k)
{
    if (f[x][k]>0) return f[x][k];
    if (!x || k<=0) return 0;
    f[b[x]][k]=dfs(b[x],k);//不取x
    f[x][k]=f[b[x]][k];
    int y=k-w[x];
    for (int i=0;i<=y;++i)
    {
        f[c[x]][y-i]=dfs(c[x],y-i);
        f[b[x]][i]=dfs(b[x],i);
        f[x][k]=max(f[x][k],v[x]+f[c[x]][y-i]+f[b[x]][i]);
    }
    return f[x][k];
}
int main()
{
    read(n);read(m);
    for (int i=1;i<=n;++i)
        read(w[i]);
    for (int i=1;i<=n;++i)
        read(v[i]);
    for (int i=1;i<=n;++i)
    {
        read(d[i]);
        a[d[i]][i]=1;
    }
    floyd();//判斷是否有環
    merge();//縮點
    for (int i=1;i<=tmpn;++i)//多叉轉二叉
        if (w[i]>0)
        {
            b[i]=c[d[i]];
            c[d[i]]=i;
        }
    printf("%d",dfs(c[0],m));
    return 0;
}

[IOI2005]Riv 河流

title

LUOGU 3354

背景 Background
世界,就是一條河流,而我,是它的國王。~河流之王塔姆
描述 Description
  幾乎整個河流之王的王國都被森林和河流所覆蓋。小點的河匯聚到一起,形成了稍大點的河。就這樣,所有的河水都匯聚並流進了一條大河,最後這條大河流進了大海。這條大河的入海口處有一個村莊——塔姆王國。
  在塔姆王國,有n個伐木的村莊,這些村莊都座落在河邊。目前在塔姆王國,有一個巨大的伐木場,它處理著全國砍下的所有木料。木料被砍下後,順著河流而被運到塔姆王國的伐木場。塔姆決定,為了減少運輸木料的費用,再額外地建造k個伐木場。這k個伐木場將被建在其他村莊裡。這些伐木場建造後,木料就不用都被送到塔姆王國了,它們可以在 運輸過程中第一個碰到的新伐木場被處理。顯然,如果伐木場座落的那個村子就不用再付運送木料的費用了。它們可以直接被本村的伐木場處理。
注:所有的河流都不會分叉,形成一棵樹,根結點是塔姆。
  塔姆的大臣計算出了每個村子每年要產多少木料,你的任務是決定在哪些村子建設伐木場能獲得最小的運費。其中運費的計算方法為:每一噸木料每千米1分錢。
  編一個程式:
  1.從檔案讀入村子的個數,另外要建設的伐木場的數目,每年每個村子產的木料的塊數以及河流的描述。
  2.計算最小的運費並輸出。
輸入格式 Input Format
  第一行包括兩個數n(2<=n<=100),k(1<=k<=50,且k<=n)。n為村莊數,k為要建的伐木場的數目。除了Bytetown 外,每個村子依次被命名為 1,2,3……n,Bytetown被命名為0。
  接下來n行,每行3個整數:
  wi——每年 i 村子產的木料的塊數。(0<=wi<=10000)
  vi——離 i 村子下游最近的村子。(即 i 村子的父結點)(0<=vi<=n)
  di——vi 到 i 的距離(千米)。(1<=di<=10000)
  保證每年所有的木料流到bytetown 的運費不超過2000,000,000分
  50%的資料中n不超過20。
輸出格式 Output Format
  輸出最小花費,精確到分。
樣例輸入 Sample Input
4 2
1 0 1
1 1 10
10 2 5
1 2 3
樣例輸出 Sample Output
4
時間限制 Time Limitation
1s
註釋 Hint
伐木場應建在村莊2和3。
來源 Source
IOI 2005 Rivers(riv)

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=201;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch) && ch^'-') ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
int w[maxn],d[maxn],f[maxn][maxn][maxn];
int brother[maxn],child[maxn],Get;
inline int dfs(int x,int y,int k,int dist)//dist表示x的父節點距離y的距離
{
	if (x==-1 || y==-1) return 0;
	if (f[x][y][k]!=Get) return f[x][y][k];
	for (int i=0;i<=k;++i)//自己不建工廠
	{
		int first=dfs(child[x],y,i,dist+d[x]);//自己的孩子要承擔i個工廠,所以到兒子處還要花費d[i]個代價
		int second=dfs(brother[x],y,k-i,dist);//自己的兄弟要承擔k-i個工廠
		int value=(dist+d[x])*w[x];//自己送到下一個節點要花費的代價
		f[x][y][k]=min(f[x][y][k],first+second+value);
	}
	for (int i=0;i<k;++i)//自己建工廠,因為已經建了一個,所以迴圈到k-1即可
	{
		int first=dfs(child[x],x,i,0);
		int second=dfs(brother[x],y,k-1-i,dist);
		f[x][y][k]=min(f[x][y][k],first+second);
	}
	return f[x][y][k];
}
int main()
{
	memset(brother,-1,sizeof(brother));
	memset(child,-1,sizeof(child));
	memset(f,0x3f,sizeof(f));
	int n,m;
	read(n);read(m);
	Get=f[0][0][0];
	for (int i=1,x;i<=n;++i)
	{
		read(w[i]);read(x);read(d[i]);
		brother[i]=child[x];
		child[x]=i;
	}
	printf("%d\n",dfs(0,0,m,0));
	return 0;
}

有線電視網

title

題目描述
某收費有線電視網計劃轉播一場重要的足球比賽。他們的轉播網和使用者終端構成一棵樹狀結構,這棵樹的根結點位於足球比賽的現場,樹葉為各個使用者終端,其他中轉站為該樹的內部節點。
從轉播站到轉播站以及從轉播站到所有使用者終端的訊號傳輸費用都是已知的,一場轉播的總費用等於傳輸訊號的費用總和。
現在每個使用者都準備了一筆費用想觀看這場精彩的足球比賽,有線電視網有權決定給哪些使用者提供訊號而不給哪些使用者提供訊號。
寫一個程式找出一個方案使得有線電視網在不虧本的情況下使觀看轉播的使用者儘可能多。
輸入輸出格式
輸入格式:
輸入檔案的第一行包含兩個用空格隔開的整數N和M,其中2≤N≤3000,1≤M≤N-1,N為整個有線電視網的結點總數,M為使用者終端的數量。
第一個轉播站即樹的根結點編號為1,其他的轉播站編號為2到N-M,使用者終端編號為N-M+1到N。
接下來的N-M行每行表示—個轉播站的資料,第i+1行表示第i個轉播站的資料,其格式如下:
K A1 C1 A2 C2 … Ak Ck
K表示該轉播站下接K個結點(轉播站或使用者),每個結點對應一對整數A與C,A表示結點編號,C表示從當前轉播站傳輸訊號到結點A的費用。最後一行依次表示所有使用者為觀看比賽而準備支付的錢數。
輸出格式:
輸出檔案僅一行,包含一個整數,表示上述問題所要求的最大使用者數。
輸入輸出樣例
輸入樣例#1:
5 3
2 2 2 5 3
2 3 2 4 3
3 4 2
輸出樣例#1:
2
說明
樣例解釋
在這裡插入圖片描述
如圖所示,共有五個結點。結點①為根結點,即現場直播站,②為一箇中轉站,③④⑤為使用者端,共M個,編號從N-M+1到N,他們為觀看比賽分別準備的錢數為3、4、2,從結點①可以傳送訊號到結點②,費用為2,也可以傳送訊號到結點⑤,費用為3(第二行資料所示),從結點②可以傳輸訊號到結點③,費用為2。也可傳輸訊號到結點④,費用為3(第三行資料所示),如果要讓所有使用者(③④⑤)都能看上比賽,則訊號傳輸的總費用為:
2+3+2+3=10,大於使用者願意支付的總費用3+4+2=9,有線電視網就虧本了,而只讓③④兩個使用者看比賽就不虧本了。

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e3+10;
template<typename T>inline void read(T &x)
{
    x=0;
    T f=1,ch=getchar();
    while (!isdigit(ch) && ch^'-') ch=getchar();
    if (ch=='-') f=-1, ch=getchar();
    while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
    x*=f;
}
int ver[maxn<<1],edge[maxn<<1],Next[maxn<<1],head[maxn],len;
inline void add(int x,int y,int z)
{
    ver[++len]=y,edge[len]=z,Next[len]=head[x],head[x]=len;
}
int n,m,degree[maxn],siz[maxn];
int money[maxn],f[maxn][maxn];
inline void dfs(int x,int fa)
{
    f[x][0]=0;
    for (int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if (y==fa) continue;
        dfs(y,x);
        siz[x]+=siz[y];
        for (int j=siz[x];j;--j)
            for (int k=1;k<=min(j,siz[y]);++k)
                f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]-edge[i]);
    }
    if (degree[x]==1)
        f[x][1]=money[x],siz[x]=1;
}
int main()
{
    read(n);read(m);
    memset(f,128,sizeof(f));
    for (int i=1,k,y,z;i<=n-m;++i)
    {
        read(k);
        while (k--)
        {
            read(y);read(z);
            add(i,y,z);add(y,i,z);
            ++degree[i],++degree[y];
        }
    }
    for (int i=n-m+1;i<=n;++i)
        read(money[i]);
    dfs(1,1);
    for (int i=m;i>=0;--i)
        if (f[1][i]>=0)
        {
            printf("%d\n",i);
            break;
        }
    return 0;
}

沒有上司的舞會

title

LUOGU 1352
CH 5401
題目描述

某大學有N個職員,編號為1~N。他們之間有從屬關係,也就是說他們的關係就像一棵以校長為根的樹,父結點就是子結點的直接上司。現在有個週年慶宴會,宴會每邀請來一個職員都會增加一定的快樂指數Ri,但是呢,如果某個職員的上司來參加舞會了,那麼這個職員就無論如何也不肯來參加舞會了。所以,請你程式設計計算,邀請哪些職員可以使快樂指數最大,求最大的快樂指數。

輸入輸出格式
輸入格式:

第一行一個整數N。(1<=N<=6000)
接下來N行,第i+1行表示i號職員的快樂指數Ri。(-128<=Ri<=127)
接下來N-1行,每行輸入一對整數L,K。表示K是L的直接上司。
最後一行輸入0 0

輸出格式:

輸出最大的快樂指數。

輸入輸出樣例

輸入樣例#1:
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
0 0

輸出樣例#1:

5

analysis

其實是道入門題,不過隔了這麼長時間才去寫,就把他放到這裡吧,不想大改動了。

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=6e3+10;
template<typename T>inline void read(T &x)
{
    x=0;
    T f=1,ch=getchar();
    while (!isdigit(ch) && ch^'-') ch=getchar();
    if (ch=='-') f=-1, ch=getchar();
    while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
    x*=f;
}
int fa[maxn],happy[maxn],In[maxn],dp[maxn][2];
int main()
{
    int n;
    read(n);
    for (int i=1;i<=n;++i)
        read(happy[i]);
    for (int i=1;i<=n;++i)
    {
        int x,y;
        read(x);read(y);
        if (!x && !y) break;
        ++In[y];
        fa[x]=y;
    }
    queue<int>q;
    for (int i=1;i<=n;++i)
        if (!In[i])
            q.push(i);
    int ans=0;
    while (!q.empty())
    {
        int x=q.front();
        q.pop();
        dp[x][1]+=happy[x];
        ans=max(ans,max(dp[x][0],dp[x][1]));
        if (!--In[fa[x]])
            q.push(fa[x]);
        dp[fa[x]][0]+=max(dp[x][0],dp[x][1]);
        dp[fa[x]][1]+=dp[x][0]>=0?dp[x][0]:0;
    }
    printf("%d\n",ans);
    return 0;
}

  1. 不撞南牆不回頭——樹規總結——焦作一中資訊學oy ↩︎

相關文章