[CEOI2017]Chase 題解
perface
先扯一點沒用的話,可以跳過。
考試的時候老師沒有給來源,下意識以為是出的原題???
然後發的 solution 因為在機房裡打打鬧鬧,電腦重啟,極域被關掉了,沒收到就以為沒有???
“幸好”之前存的 std 沒有被還原掉,於是就直接盯著 std 硬看,猜思路,猜狀態,瘋狂畫圖
最後實在沒辦法,請教機房的大佬,然後大佬直接對著 luogu 上的題面給我講???
woc ,這個不是原題???(白盯了這麼久)
於是決定寫篇題解紀念一下。
題解的大致邏輯是考場思路(當然我考場並沒有做出來)
Statement
Solve
考試的時候想到了的性質 :
- 起點一定會用一個,用完肯定走
顯然,如果起點不用,那麼不如直接在使用第一個磁鐵的地方開始
因為在另外一個地方後使用磁鐵貢獻是 \(st^{\prime}\) 周圍 \(f\) 的和減去紅點的 \(f\)
而直接在 \(st^{\prime}\) 使用磁鐵則不需要減去紅色部分的值
顯然,磁鐵用完後,貢獻不會增加。
這個性質可以引導我們列舉起點終點 就這 ,考試的時候也並不是很會用
-
假如一個點 \(u\) 被放置了磁鐵,那麼它的貢獻可以表示為 \(\sum_{(u\to v)} f[v] \ \ \ \ -f[pre]\)
其中 \(pre\) 是點 \(u\) 的上一個點。
能不能再挖掘出什麼有用的性質呢?布吉島,先打暴力再說
20pts
暴力列舉嘛,終點起點,放或不放,\(O(n^22^n)\)
40pts
考慮樹上 \(dp\)
設 \(dp[i][j][0/1]\) 表示以 \(rt\) 為根的樹,點 \(i\) 到子樹內某一點,用了 \(j\) 個磁鐵,點 \(i\) 用不用磁鐵的最大貢獻,那麼
\[dp[u][j][0]=\max\{dp[v][j][0],dp[v][j][1]\}\\ dp[u][j][1]=\max\{\max(dp[v][j-1][0],dp[v][j-1][1])+sum[u]\} \]其中 \(sum[u]\) 表示的是以 \(rt\) 為根的樹中,\(\sum f[son_u]\)
第一個沒啥好說的
第二個主要是看 \(sum[u]\) 什麼意思,因為是從 \(u\) 到子樹內,所以選 \(u\) 的時候,\(f[fath]+\sum f[son]\) 都會被吸引,但是因為我們是從 \(fath\) 走過來的,所以 \(f[fath]\) 沒有貢獻,其他都有貢獻,剛剛好就是 \(sum[u]\)
這樣一遍是 \(O(nv)\) 的,發現 \(rt\) 的意義和起點相同,所以還要列舉起點 \(rt\) ,總共就是 \(O(n^2v)\)
思考發現,好像最後半維 \([0/1]\) 是沒必要的,上面兩個式子可以合併成:
\[dp[u][j]=\max\{dp[v][j],dp[v][j-1]+sum[u]\} \]這可以小小優化常數,還可以加入優化:磁鐵為 \(0\) 則離開
70pts
即固定 \(rt=1\) (但是怎麼判斷呢)
100pts
考慮對 \(dp\) 進行優化
發現整個過程最愚蠢的地方就是列舉 \(rt\) ,所以考慮換根???
@#¥%……&* 亂推勾 \(8\) 了一下式子,發現這個 \(\max\) 不會處理 \(50pts\) 走人。
好的,現在賽後來看,大體有兩種思路:
Way1
延續換根 \(dp\) 的思想,考慮如何強行處理 \(\max\)
假設我們已經求到了 \(rt=fath\) 意義下的 \(dp[i][j]\)
考慮最後得到的 \(rt=u\) 意義下的路徑會是怎麼樣:
會是從 \(u\to v_1\) 或者 \(u\to v_2\)
這引導我們把 \(fath\) 的所有子樹拍進一個 \(vector\) 裡面
然後維護 \(dp[v][j](v\in son_{fath})\) 的字首 \(pre\),字尾 \(suf\) 最大值
那麼圖中紅線貢獻 \(=pre+(sum[fath]+f[father]-f[u])+(sum[u]+f[fath])\)
那麼圖中藍線貢獻 \(=suf+(sum[fath]+f[father]-f[u])+(sum[u]+f[fath])\)
上式第一組括號內是 \(fath\) 的貢獻,第二組是 \(u\to fath\) 的貢獻
當然,上面的式子是在 \(fath\) 和 \(u\) 都放磁鐵的情況。
接下來,我覺得就可以直接通過程式碼理解了。
Code1
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5+5;
const int M = 1e2+5;
struct Edge{
int nex,to;
}edge[N<<1];
int head[N],dp[N][M],sum[N],f[N];
int n,m,elen,ans;
void addedge(int u,int v){
edge[++elen]={head[u],v},head[u]=elen;
edge[++elen]={head[v],u},head[v]=elen;
}
void dfs(int u,int fath){
for(int e=head[u],v;e;e=edge[e].nex)
if((v=edge[e].to)!=fath)
sum[u]+=f[v],dfs(v,u);
for(int e=head[u],v;e;e=edge[e].nex)
if((v=edge[e].to)!=fath)
for(int i=1;i<=m;++i)
dp[u][i]=max(dp[u][i],max(dp[v][i],dp[v][i-1]+sum[u]));//在 rt=1 意義下的 dp 求解
}
void dfs2(int u,int fath){
vector<int>son,pre[M];int suf[M];
memset(suf,0,sizeof(suf));
//son 記錄所有與 u 相連的點(包括 fath)
//pre[i] 記錄使用 i 個磁鐵的字首最大值
//suf[i] 記錄使用 i 個磁鐵的字尾最大值
for(int e=head[u],v;e;e=edge[e].nex){
son.push_back(v=edge[e].to);
for(int i=1;i<=m;++i)
ans=max(ans,dp[u][i]=max(dp[u][i],max(dp[v][i],dp[v][i-1]+sum[u]+f[fath]))),//在 rt=u 的意義下(起點為u)的 dp 陣列,注意在起點時,周圍所有點都可以貢獻
dp[u][i]=0;//清零,為兒子的求解做準備
}
for(int i=0,tmp=0;i<=m;++i,tmp=0)
for(auto v:son)
tmp=max(tmp,dp[v][i]),
pre[i].push_back(tmp);//pre[i][j] 表示前 j 個兒子,使用 i 個磁鐵最大值
for(int i=(int)son.size()-1;i;--i){
int v=son[i];
for(int j=m;j;--j)//倒序列舉,方便得到 suf
dp[u][j]=max(dp[u][j],max(max(suf[j],suf[j-1]+sum[u]+f[fath]-f[v]),
max(pre[j][i-1],pre[j-1][i-1]+sum[u]+f[fath]-f[v]))),//放/不放磁鐵
suf[j]=max(suf[j],dp[v][j]);
//此時, dp[u][j] 的意義變成了從 u 走到子樹內 v^{\prime} (v^{\prime}\neq v) 的最大值
if(v!=fath)dfs2(v,u);//////////////////
for(int j=1;j<=m;++j)dp[u][j]=0;
}
if(son.size()){//第一個兒子,沒有辦法用 pre 更新
int v=son[0];
for(int j=1;j<=m;++j)
dp[u][j]=max(dp[u][j],max(suf[j],suf[j-1]+sum[u]+f[fath]-f[v]));
if(v!=fath)dfs2(v,u);///////////////
}
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;++i)scanf("%lld",&f[i]);
for(int i=1,u,v;i<n;++i)
scanf("%lld%lld",&u,&v),addedge(u,v);
dfs(1,0);
dfs2(1,0);
printf("%lld\n",ans);
return 0;
}
Way2
我們可以轉化問題:一棵樹,點有點權,求在一個計演算法則下的直徑
(所謂計演算法則就是磁鐵的使用)
考慮常規樹的直徑是怎麼做的:設 \(d1[u],d2[u]\) 分別表示從 \(u\) 走到 \(u\) 子樹內的最長路和次長路
那麼,\(ans=\max\{d1[u]+d2[u]\}\)
嘗試套到本題,特殊的是,從 上往下走 和 從下往上 走同一條路徑的貢獻並不相同
那麼,我們設 \(up[i][j][0/1]\) 表示從 \(i\) 的子樹內走到 \(i\) ,使用 \(j\) 塊磁鐵, \(i\) 放不放磁鐵的最大貢獻
同理,設 \(down[i][j][0/1]\)
容易寫出狀態轉移方程:
\[\begin{align} up[u][i][0]& = \max(up[u][i][0], \max(up[v][i][0], up[v][i][1]))\\ up[u][i][1] &= \max(up[u][i][1], \max(up[v][i - 1][0], up[v][i - 1][1]) + sum[u] - f[v] + f[fath])\\ down[u][i][0]& = \max(down[u][i][0], \max(down[v][i][0], down[v][i][1]))\\ down[u][i][1]& = \max(down[u][i][1], \max(down[v][i - 1][0], down[v][i - 1][1]) + sum[u]) \end{align} \]初態:
up[u][0][1] = down[u][0][1] = -inf;
up[u][1][1] = max(up[u][1][1], sum[u] + f[fath]);
因為顯然。
答案可以通過字首最大值更新
Code2
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
const int inf = 1e18;
struct Edge {
int nex, to;
} edge[N << 1];
int head[N], f[N], sum[N];
int up[N][105][2], down[N][105][2];
int n, V, elen, ans;
void addedge(int u, int v)
{
edge[++elen] = { head[u], v }, head[u] = elen;
edge[++elen] = { head[v], u }, head[v] = elen;
}
void DP(int u, int fath)
{
for (int e = head[u], v; e; e = edge[e].nex) {
if ((v = edge[e].to) == fath)
continue;
DP(v, u);
sum[u] += f[v];
}
up[u][0][1] = down[u][0][1] = -inf;
up[u][1][1] = max(up[u][1][1], sum[u] + f[fath]);
for (int e = head[u], v; e; e = edge[e].nex) {
if ((v = edge[e].to) == fath)
continue;
int mx1 = 0, mx2 = 0;
for (int i = V; ~i; --i) {
mx1 = max(mx1, max(up[u][V - i][0], up[u][V - i][1]));
mx2 = max(mx2, max(down[u][V - i][0], down[u][V - i][1] + f[fath] - f[v]));
ans = max(ans, max(down[v][i][0], down[v][i][1]) + mx1);
ans = max(ans, max(up[v][i][0], up[v][i][1]) + mx2);
}
for (int i = 1; i <= V; ++i)
up[u][i][0] = max(up[u][i][0], max(up[v][i][0], up[v][i][1])),
up[u][i][1] = max(up[u][i][1], max(up[v][i - 1][0], up[v][i - 1][1]) + sum[u] - f[v] + f[fath]),
down[u][i][0] = max(down[u][i][0], max(down[v][i][0], down[v][i][1])),
down[u][i][1] = max(down[u][i][1], max(down[v][i - 1][0], down[v][i - 1][1]) + sum[u]);
}
}
signed main()
{
scanf("%lld%lld", &n, &V);
for (int i = 1; i <= n; ++i)
scanf("%lld", &f[i]);
for (int i = 1, u, v; i < n; ++i)
scanf("%lld%lld", &u, &v), addedge(u, v);
DP(1, 0);
printf("%lld\n", ans);
return 0;
}