【做題】SDOI2017蘋果樹——dfs序的運用
原文鏈接 https://www.cnblogs.com/cly-none/p/9845046.html
題意:給出一棵\(n\)個結點的樹,在第\(i\)個結點上有\(a_i\)個權值為\(v_i\)的物品。\(1\)號結點是根結點。你需要選出若幹個物品(設選了\(t\)個),滿足:
- 如果選了結點\(i\)上的物品,那麽\(i\)到根的鏈上每個結點都至少要選一個物品。
- 設有選取物品的結點的最大深度為\(h\),那麽\(t \leq h + k\),\(k\)為一個給定的常數。
在此基礎上,你需要最大化所選的物品的權值和。
\(n \leq 2 \times 10^4, \, k \leq 5 \times 10^5, \, n \times k \leq 2.5 \times 10^7\)
顯然,最終做法的復雜度應該是\(O(nk)\)的。
但這個問題比較復雜,直接想比較困難。因此,我們先考慮問題的簡化版。
問題1
當第二個條件改為\(t \leq k\)時,怎麽做?
對於這種一個結點的決策影響其子樹的問題,我們可以對dfs序倒過來dp。確切地說,考慮當前是\(i\),那麽\(i\)的子樹就是\(dfn_i\)之後的一段連續區間。那麽,把dfs序倒過來後,結點\(i\)就有兩種可能:
- 選了\(i\)上的物品。就是一個多重背包,從\(dp_{dfn_i + 1}\)上更新過來。
- 不選\(i\)上的物品。那\(i\)子樹中的所有物品都不能選。從\(dp_{dfn_i + sz_i}\)
用單調隊列優化多重背包後,就能做到\(O(nk)\)。
然而,回過頭來,我們依舊對\(t \leq h + k\)感到棘手。嘗試按常規方法dp對\(k+h-t\)記錄答案,但沒有用。這個限制其實就在於,選出一條一段是根結點的鏈,鏈上每個點都取一個不計入\(t\)的物品。我們設這條鏈除\(1\)外的端點為\(x\)。考慮\(\forall i, \, a_i = 1\)的部分分。那麽,假如我們已經確定了\(x\),則剩下的答案就是刪去\(1\)到\(x\)的鏈,對剩下的森林做問題1的結果。
因此,我們可以考慮下面這個問題:
問題2
預處理:對於所有\(x\),刪去\(x\)到根的路徑後剩下的森林的問題1的答案。
博主認為,這個問題的解法相當有趣,也挺難想到的。
考慮剩下的森林的一半就是在dfs序上,從\(dfn_i + 1\)到\(n\)的一段區間(包括了\(i\)的子樹)。這個部分我們在dp時就已經把答案求出來了。然而,另一部分在dfs序上既不是一段後綴,也不是連續的區間。\([1,dfn_i-1]\)中還混入了\(i\)的所有祖先。
因此,我們把這棵樹左右翻轉,把剩下森林的兩半交換位置。也就是,再生成一個dfs序,但每個結點反序訪問它的孩子結點。這樣,我們就把森林的另一部分也表示為了dfs序的一個後綴。值得註意的是,\(i\)的子樹不能算兩次,所以這個後綴應該是[dfn_i + sz_i,n]。
這樣,我們做出兩個dfs序,對每個做問題1的dp,就能解決此問題。
然後就是處理\(a_i \neq 1\)的情況。上面的算法會錯誤,就在於\(x\)到根的路徑上的結點,可能選了多個物品。那麽,我們就對每個結點\(i\)建一個輔助點\(i'\),存放了\(a_i - 1\)個原來在\(i\)上的物品。這樣,對於任何一個非輔助結點,它到根的路徑上所有點都只有一個物品。
這樣就能把最終問題轉化為問題1,\(O(nk)\)地解決本題。
#include <bits/stdc++.h>
using namespace std;
const int N = 40010, K = 500010, SIZE = 51000010;
int n,k,val[N],num[N],dfn[N],sz[N],fa[N],cnt,dis[N],ans,rec[N],spadp[SIZE],spag[SIZE];
vector<int> ch[N];
int *dp[N],*g[N];
void dfs(int pos) {
sz[pos] = 1;
for (int i = 0 ; i < (int)ch[pos].size() ; ++ i) {
dfs(ch[pos][i]);
sz[pos] += sz[ch[pos][i]];
}
dfn[rec[pos] = ++cnt] = pos;
}
void fsd(int pos) {
dis[pos] += val[pos];
for (int i = (int)ch[pos].size() - 1 ; i >= 0 ; -- i) {
dis[ch[pos][i]] = dis[pos];
fsd(ch[pos][i]);
}
dfn[++cnt] = pos;
}
void update(int las,int cur) {
static int q[K],l,r;
l = 1, r = 0;
q[++r] = 0;
for (int i = 1 ; i <= k ; ++ i) {
while (l <= r && i - q[l] > num[dfn[cur]])
++ l;
if (l <= r)
dp[cur][i] = dp[las][q[l]] + val[dfn[cur]] * (i - q[l]);
else dp[cur][i] = 0;
while (l <= r && dp[las][i] > dp[las][q[r]] + val[dfn[cur]] * (i - q[r]))
-- r;
q[++r] = i;
}
}
void init() {
ans = 0;
for (int i = 0 ; i <= 2 * n ; ++ i) {
ch[i].clear();
dp[i] = spadp + i * (k + 1);
g[i] = spag + i * (k + 1);
memset(dp[i],0,sizeof(int) * (k + 1));
memset(g[i],0,sizeof(int) * (k + 1));
}
dis[1] = 0;
}
int main() {
int T;
scanf("%d",&T);
while (T --) {
scanf("%d%d",&n,&k);
init();
for (int i = 1 ; i <= n ; ++ i)
scanf("%d%d%d",&fa[i],&num[i],&val[i]);
for (int i = 2 ; i <= n ; ++ i)
ch[fa[i]].push_back(i);
for (int i = 1 ; i <= n ; ++ i) {
ch[i].push_back(i+n);
val[i+n] = val[i];
num[i+n] = num[i] - 1;
num[i] = 1;
}
cnt = 0;
dfs(1);
for (int i = 1 ; i <= 2 * n ; ++ i) {
update(i-1,i);
for (int j = 1 ; j <= k ; ++ j)
dp[i][j] = max(dp[i][j],dp[i - sz[dfn[i]]][j]), dp[i][j] = max(dp[i][j],dp[i][j-1]);
}
for (int i = 1 ; i <= 2 * n ; ++ i)
for (int j = 1 ; j <= k ; ++ j)
g[i][j] = dp[i][j];
cnt = 0;
fsd(1);
for (int i = 1 ; i <= 2 * n ; ++ i) {
update(i-1,i);
for (int j = 1 ; j <= k ; ++ j)
dp[i][j] = max(dp[i][j],dp[i - sz[dfn[i]]][j]), dp[i][j] = max(dp[i][j],dp[i][j-1]);
}
for (int i = 1 ; i <= 2 * n ; ++ i) {
if (dfn[i] > n) continue;
int p = rec[dfn[i]] - sz[dfn[i]];
for (int j = 0 ; j <= k ; ++ j)
ans = max(ans,dis[dfn[i]] + dp[i-1][j] + g[p][k-j]);
}
printf("%d\n",ans);
}
return 0;
}
小結:一道對dfs序上dp進行拓展的好題。當一個問題分成了性質相同的兩半,而前者容易解決,後者難以解決的問題時,尋找方式來交換這兩部分的位置,最後合並。這個思路應該記住。
【做題】SDOI2017蘋果樹——dfs序的運用