1. 程式人生 > 實用技巧 >[CF791D]Bear and Tree Jumps 樹上DP

[CF791D]Bear and Tree Jumps 樹上DP

題面:

給定一顆大小為\(n\)的樹,給定一個k,求\(\sum_{i=1}^n \sum_{j=1}^n \lceil \frac{dis(i,j)}{k} \rceil\)

範圍&性質:\(1\le n \le 2\times 10^5,1\le k\le 5\)

分析:

樸素暴力做法:

對於每一個點dfs一遍,統計答案,複雜度\(O(n^2)\)

正解:(前排膜拜 Orz)

先考慮k=1怎麼做,相當於求樹上任意兩點間路徑和,直接列舉每條邊,求出左右兩部分內點的個數\(num1,num2\),相乘就好了,複雜度\(O(n)\)

那怎麼向k>1擴充套件呢,問題在於k>1時路徑長度除以k會有餘數,那我們就考慮把餘數補齊,答案就變成了 \(\frac{ans+\sum f(i,j)}{k}\)

,其中\(f(i,j)\)表示將\(dis(i,j)\)補成k的倍數需要的代價,這樣就可以消掉上取整符號的影響了

那麼問題就轉化成了如何求\(\sum f(i,j)\),這時候就利用到k很小這一性質,我們記\(dp[i][j]\)表示u的子樹內到根節點距離\(k\)\(j\)的點的個數,對於樹上任意兩點\(a,b\)而言,他們之間的距離\(dis(a,b)=dep[a]+dep[b]-2*dep[lca]\)\(f(a,b)=(dis(a,b)-k)mod \ k\),轉移的時候我們就可以列舉\(u,v\)中dp狀態第二維\(i,j\),這些點之間的距離\(dis=(i+j-dep[u]) mod\ k\)

(因為\(u,v\)\(lca\)\(u\))單個點對需要的代價\(f=(dis-k)mod\ k\),由乘法原理得總的代價為\(f(i,j)\times dp[u][i]\times dp[v][j]\)

程式碼:

#include<bits/stdc++.h>

using namespace std;

namespace zzc
{
	const int maxn = 200005;
	int cnt=0,head[maxn];
    long long dp[maxn][5],siz[maxn],ans=0;
    int k,n;
	
	struct edge
    {
    	int to,nxt;
	}e[maxn<<1];
	
	void add(int u,int v)
	{
		e[++cnt].to=v;
		e[cnt].nxt=head[u];
		head[u]=cnt;
	}
	
	void dfs(int u,int fa,int dep)
	{
		dp[u][dep%k] = 1;siz[u]=1;
		for (int i=head[u];i;i=e[i].nxt)
		{
			int v=e[i].to;
			if (v==fa) continue;
			dfs(v,u,dep+1);
			for (int j = 0;j < k;j++)
			{
				for (int r = 0;r < k;r++)
				{
					int dis = (j + r - dep * 2) % k;
					int rev = (k - dis) % k;
					ans += rev*dp[u][j] * dp[v][r];
				}
			}
			siz[u]+=siz[v];
			for (int j = 0;j < k;j++) dp[u][j] += dp[v][j];
			ans += (n - siz[v])*siz[v];
		}
	}

	
	void work()
	{
		scanf("%d%d",&n,&k);
		for (int i=1,x,y;i < n;i++)
		{
			scanf("%d%d",&x,&y);
			add(x,y);add(y,x);
		}
		dfs(1, -1, 0);
		printf("%lld\n",ans/k);
	}
	
}

int main()
{
	zzc::work();
	return 0;
}