1. 程式人生 > 其它 >【CF1039D】You Are Given a Tree

【CF1039D】You Are Given a Tree

題目

題目連結:https://codeforces.com/contest/1039/problem/D
有一棵 \(n\) 個節點的樹。其中一個簡單路徑的集合被稱為 \(k\) 合法當且僅當樹的每個節點至多屬於其中一條路徑,且每條路徑恰好包含 \(k\) 個點。
對於 \(k\in [1,n]\),求出 \(k\) 合法路徑集合的最多路徑數。
即:設 \(k\) 合法路徑集合為 \(S\),求最大的 \(|S|\)
\(n \leq 10^5\)

思路

考慮當 \(k\) 固定的時候怎麼搞。設 \(f[x]\) 表示 \(x\) 子樹內到 \(x\) 沒有點用過的一條路徑最長的長度。合併兩棵子樹時,如果 \(f[x]+f[y]+1\geq k\)

,那麼就貪心合併這兩條路徑並讓答案加一,\(f[x]\) 賦值為負無窮。
這樣的貪心策略正確性顯然,因為如果可以合併但是不合並而是把這一段長度給到父節點肯定不會更優。
暴力做是 \(O(n^2)\) 的。考慮根號分治。
設一個閾值 \(M\)。當 \(k\leq M\) 的時候暴力求。當 \(k>M\) 的時候,答案肯定在 \([0,\lfloor\frac{n}{M}\rfloor]\) 內,而隨著 \(k\) 的增大,答案是不增的。
那麼假設當前求 \(i\) 的答案 \(res\),我們可以二分一個右端點 \(j\) 滿足它是最大的答案等於 \(res\) 的點。那麼 \([i,j]\)
的答案一定都等於 \(res\)
二分次數是 \(O(\frac{n}{M})\) 的,每一次二分的複雜度都是 \(O(n\log n)\)。再加上前面暴力的複雜度,總時間複雜度為 \(O(nM+\frac{n^2\log n}{M})\)。取 \(M=\sqrt{n\log n}\) 時有最優複雜度 \(O(n\sqrt{n\log n})\)
這道題有點卡常,可以按照 dfs 序來迴圈 dp。

程式碼

#include <bits/stdc++.h>
using namespace std;

const int N=100010,M=1300,Inf=1e9;
int n,tot,head[N],dfn[N],fa[N],f[N];

struct edge
{
	int next,to;
}e[N*2];

void add(int from,int to)
{
	e[++tot]=(edge){head[from],to};
	head[from]=tot;
}

void dfs(int x,int fat)
{
	dfn[++tot]=x; fa[x]=fat;
	for (int i=head[x];~i;i=e[i].next)
	{
		int v=e[i].to;
		if (v!=fat) dfs(v,x);
	}
}

int solve(int k)
{
	int ans=0;
	for (int j=n;j>=1;j--)
	{
		int x=dfn[j]; f[x]=0;
		for (int i=head[x];~i;i=e[i].next)
		{
			int v=e[i].to;
			if (v!=fa[x])
			{
				if (f[v]+f[x]+1>=k) ans++,f[x]=-Inf;
				if (f[x]!=-Inf) f[x]=max(f[x],f[v]);
			}
		}
		f[x]++;
	}
	return ans;
}

int main()
{
	memset(head,-1,sizeof(head));
	scanf("%d",&n);
	for (int i=1,x,y;i<n;i++)
	{
		scanf("%d%d",&x,&y);
		add(x,y); add(y,x);
	}
	tot=0; dfs(1,0);
	cout<<n<<"\n";
	for (int i=2;i<=min(n,M);i++)
		cout<<solve(i)<<"\n";
	for (int i=M+1;i<=n;)
	{
		int l=i,r=n,mid,res=solve(i);		
		while (l<=r)
		{
			mid=(l+r)>>1;
			if (solve(mid)==res) l=mid+1;
				else r=mid-1;
		}
		for (;i<l;i++) cout<<res<<"\n";
	}
	return 0;
}