1. 程式人生 > >【三校聯考10.24】點亮

【三校聯考10.24】點亮

@點亮@


@問題描述@

間宮卓司種下一個小樹苗,它長成了一棵 n 個點的有根樹,根節點為 1,點 u 的父節點為 p u

p_u 。由於在生長過程中沒有加以人工干預,所以這棵樹的形態有一定的隨機性:保證 p u p_u 是在 1 到 u − 1 中等概率隨機選取的。現在,這棵樹的所有節點都黯淡無光。他要使用救世主的法力來點亮一些點,使這棵樹變得更加美麗。
對於有序點對 (u, v)(u≠v),如果 u 沒有被點亮且以 lca(u, v) 為根的子樹中沒有被點亮的點數大於被點亮的點數,那麼會貢獻 a
[ u ] [ v ] a[u][v]
的美麗度;如果 u 被點亮,且以 lca(u, v) 為根的子樹中被點亮的點數大於等於沒有被點亮的點數,則會貢獻 b
[ u ] [ v ] b[u][v]
的美麗度;否則不貢獻美麗度。貢獻的美麗度可以為負數。
間宮卓司想知道,通過點亮一些點,能得到整棵樹的美麗度最大是多少。

輸入
第一行,一個正整數 n。
第二行,n − 1 個正整數 p 2 , p 3 , . . . , p n p_2, p_3, . . . , p_n 。保證 p u p_u 是在 1 到 u − 1 中等概率隨機選取的。
接下來 n 行,第 u 行有 2(n−1) 個數,分別為 a [ u ] [ 1 ] , b [ u ] [ 1 ] , . . . , a [ u ] [ u 1 ] , b [ u ] [ u 1 ] , a [ u ] [ u + 1 ] , b [ u ] [ u + 1 ] , . . . , a [ u ] [ n ] , b [ u ] [ n ] a[u][1], b[u][1], . . ., a[u][u−1], b[u][u − 1], a[u][u + 1], b[u][u + 1], . . ., a[u][n], b[u][n] (即去掉 v = u 後的 n − 1對)。

輸出
一個整數表示能得到整棵樹的美麗度的最大值。

樣例輸入1
2
1
-71 69
100 -47
樣例輸出1
69
解釋:最優方案是點亮 1 不點亮 2,此時 (1, 2) 的貢獻為 69,(2, 1) 沒有貢獻。

樣例輸入2
3
1 1
-32 19 84 21
-20 0 7 -86
-37 -33 16 -66
樣例輸出2
39
3.4.3
解釋:最優方案是不點亮 1, 2,點亮 3。

資料限制
對於 40% 的資料, 1 n 16 1 ≤ n ≤ 16
對於 80% 的資料, 1 n 200 1 ≤ n ≤ 200
對於 100% 的資料, 1 n 1000 100 a [ u ] [ v ] , b [ u ] [ v ] 100 1 ≤ n ≤ 1000,−100 ≤ a[u][v], b[u][v] ≤ 100 ,保證 p u p_u 是在 1 1 u 1 u − 1 中等概率隨機選取的。

@分析1 - 隨機的性質@

【題解中提到:“眾所周知,按此方法隨機得到的樹有幾個常用性質:……”】
【恩我為啥什麼都不知道???】

好的……既然題目中多次強調樹是隨機生成的,那麼這一定是有用的。在這道題,隨機樹有以下性質可供我們使用:
1)點 i 的期望深度為 1/1 + 1/2 + 1/3+ … + 1/i ≈ ln i
2)以點 i 為根的子樹期望大小 \le n/i
【其實這種樹上每個節點的期望度數也是log級別的……只是這道題沒有用到。】

這幾個性質怎麼證明呢?如果是大佬當然可以直接用數學方法推導或者用期望 dp 快速秒掉。但其實可以寫一個生成隨機樹的程式,多刷幾組驗證一下,大概是那個數量級就OK。(hhhh我真是機智)

但是……emmm深度還好說,誰知道會根據子樹大小來設計演算法啊……

@分析2 - 利用性質@

為方便表述,如果以 i 為根的子樹點亮的點多,我們稱它為白點;否則為黑點。

我們對於每一個節點,計算它對答案的貢獻。但是這個演算法的關鍵點是,我們不知道它祖先的顏色,自上而下的演算法不太適用。

這個時候應該將思維暴力一點:既然每個點的期望深度是log級別的,那我們不妨就設計一個以深度為底數的指數級演算法來求解。深度是log級別的等價於每個點只會有log個祖先,於是就和上面的討論有關聯了:我們可以二進位制列舉每個節點祖先的顏色,計算該節點的貢獻,進行狀壓dp。

還沒完。我們要判斷一個子樹是不是真的是白色的,不能你說它是白色的它就是白色的對吧。所以我們要將這顆子樹內被點亮的點數存入狀態進行轉移。眼看著這個時間複雜度飆升,於是第二個性質就有用了:因為期望深度n/i,根據一些玄學的時間複雜度證明,它的時間複雜度竟然還是穩定在O(n^2)的。

具體細節請往下翻。

@演算法細節@

定義 d p [ i ] [ j ] [ s ] dp[i][j][s] 表示以 i 為根的子樹有 j 個被點亮的點,且從 i 到根的路徑上點的黑白狀態為 s 時的最大答案值。
定義 f [ 0 / 1 ] [ i ] [ s ] f[0/1][i][s] 表示當 i 被點亮/沒有被點亮,且從 i 到根的路徑上點的黑白狀態為 s 時 i 對答案的貢獻。
定義 g [ 0 / 1 ] [ i ] [ j ] g[0/1][i][j] 表示當 i 被點亮/沒有被點亮時,所有滿足lca(i, k) = (i的第 j 個祖先)的k, a [ i ] [ k ] / b [ i ] [ k ] a[i][k]/b[i][k] 之和。

g g 陣列可以靠列舉節點+計算lca在O(n^2log n)的時間內求解出來。
利用 g g 可以在O(n*2^logn) = O(n^2)的時間內求解 f f
轉移 d p [ i ] [ j ] [ s ] dp[i][j][s] 時可以以 j 為容量做樹上揹包。初始狀態為 d p [ 0 / 1 ] [ i ] [ 0 / 1 ] = f [ 0 / 1 ] [ i ] [ s ] dp[0/1][i][0/1]=f[0/1][i][s]

以 i 為根時,樹上揹包的時間複雜度O(siz[i]2),狀態有O(2dep[i])個。總時間複雜度 O ( i = 1 i n s i z [ i ] 2 2 d e p [ i ] ) = O ( n 2 log n ) O(\sum_{i=1}^{i\le n}siz[i]^2*2^{dep[i]})=O(n^2*\log n)
至於為什麼可以參考下面的證明。

強烈建議看一下程式碼的實現細節,不然可能寫得很醜。

@程式碼@

雖然大致思路是這樣的,但是還是有一些細節必須提一提:
1)dp陣列如果真的那麼定義,雖然時間沒問題,但空間會爆炸……正確做法是將狀態 s 作為一個dfs的引數傳遞下去。因為一個狀態 s 對於它的父節點只會產生一次貢獻,用過一次過後就可以直接覆蓋掉了。
2)不是0/1揹包或者完全揹包,所以要滾動陣列……
3)f陣列要用vector,比著子樹大小開,不然也會爆空間……
4)因為一個點作為白點和作為黑點兩種情況時它們子樹中含有的點亮的點的個數不一樣,可以先統一將它們存入某一陣列h,再進行揹包。
5)記得刪除不合法的狀態值。
6)事實上求解深度和子樹大小不需要dfs,因為它的父親節點編號一定小於它,所以可以用遞推來進行求解。
7)求lca也不需要倍增。因為每個節點的期望深度是log級別的……就直接暴力好了。

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int INF = (1<<30);
const int MAXN = 1000;
struct edge{
	int to;
	edge *nxt;
}edges[2*MAXN + 5], *adj[MAXN + 5], *ecnt=&edges[0];
void addedge(int u, int v) {
	edge *p = (++ecnt);
	p->to = v, p->nxt = adj[u], adj[u] = p;
}
int dep[MAXN + 5], siz[MAXN + 5];
int fa[MAXN + 5], a[2][MAXN + 5][MAXN + 5];
int dp[2][MAXN + 5][MAXN + 5], g[2][MAXN + 5][30], h[MAXN + 5][MAXN + 5];
vector<int>f[2][MAXN + 5];
void Copy(int x) {
	for(int i=0;i<=siz[x];i++)
		dp[0][x][i] = dp[1][x][i];
}
void Init1(int x) {
	for(int i=0;i<=siz[x];i++)
		dp[1][x][i] = -INF;
}
void Init2(int x) {
	for(int i=0;i<=siz[x];i++)
		h[x][i] = -INF;
}
void dfs(int rt, int s) {
	Init1(rt); 
	dp[1][rt][0] = f[0][rt][s];
	dp[1][rt][1] = f[1][rt][s];
	Copy(rt);
	int tot = 1;
	for(edge *p=adj[rt];p!=NULL;p=p->nxt) {
		Init1(rt); Init2(p->to);
		dfs(p->to, s<<1); dfs(p->to, s<<1|1);
		tot += siz[p->to];
		for(int i=0;i<=siz[p->to];i++)
			for(int j=i;j<=tot;j++)
				dp[1][rt][j] = max(dp[1][rt][j], dp[0][rt][j-i] + h[p->to][i]);
		Copy(rt);
	}
	if( s&1 ) {
		for(int i=(siz[rt]+