1. 程式人生 > 其它 >[CEOI2017]Chase 題解

[CEOI2017]Chase 題解

perface

先扯一點沒用的話,可以跳過。

考試的時候老師沒有給來源,下意識以為是出的原題???

然後發的 solution 因為在機房裡打打鬧鬧,電腦重啟,極域被關掉了,沒收到就以為沒有???

“幸好”之前存的 std 沒有被還原掉,於是就直接盯著 std 硬看,猜思路,猜狀態,瘋狂畫圖

最後實在沒辦法,請教機房的大佬,然後大佬直接對著 luogu 上的題面給我講???

woc ,這個不是原題???(白盯了這麼久)

於是決定寫篇題解紀念一下。

題解的大致邏輯是考場思路(當然我考場並沒有做出來)

Statement

Chase

Solve

考試的時候想到了的性質 :

  1. 起點一定會用一個,用完肯定走

顯然,如果起點不用,那麼不如直接在使用第一個磁鐵的地方開始

因為在另外一個地方後使用磁鐵貢獻是 \(st^{\prime}\) 周圍 \(f\) 的和減去紅點的 \(f\)

而直接在 \(st^{\prime}\) 使用磁鐵則不需要減去紅色部分的值

顯然,磁鐵用完後,貢獻不會增加。

這個性質可以引導我們列舉起點終點 就這 ,考試的時候也並不是很會用

  1. 假如一個點 \(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

參考:CEOI2017 Chase - starsing

延續換根 \(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

參考:Chase - zero4338 的部落格

我們可以轉化問題:一棵樹,點有點權,求在一個計演算法則下的直徑

(所謂計演算法則就是磁鐵的使用)

考慮常規樹的直徑是怎麼做的:設 \(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;
}