F
当我告诉我的同学们这题是树形dp时他们都表现出了“树?!下次一定学”的反应。于是这篇题解诞生了。
在我的理解中,树形dp的“树形”就是个“形”,把邻接表和dfs放上去,“树形”就处理完了,内核其实还是dp。
父任务:以父节点为根的树要删多少边。子任务:以子节点为根的树要删多少边。
父任务与子任务的关系(状态转移):当一个节点作为子节点删边时,若其删掉自己与父节点的公共边,那么当父节点需要删边时就能节省需要删的边;否则,不能节省。所以子节点删边时要尽可能删自己与父节点的公共边。相应的,当一个节点作为父节点时,就必须删掉其子节点们要求删掉的这些公共边。
在AC代码上加了点注释:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int N=1e5+7;
typedef long long ll;
vector<int>G[N];//邻接表
int n,k;
ll ans=0;//int dp[N];因为计算过程中用不到子树需要删除的边的数量,所以我就没开这个dp数组
int dfs(int fa,int now){
int res=0;
for(auto son:G[now]){
if(son==fa)continue;//防止访问时反复横跳
res+=dfs(now,son);
//dp[now]+=dp[son];
}
ans+=res;//dp[now]+=res;先删掉子节点们要求删的公共边
int root=G[now].size();//这个变量只是随手取名为root,显然它与“根”没有任何关系
if(root-res>k){
ans+=((root-res)-k-1);//dp[now]+=((root-res)-k-1);剩下要删的边中,一条边给父节点删,其他的随便删
return 1;//需要删掉其与父节点的公共边
}
return 0;//不需要删掉其与父节点的公共边
}
void solve(){
ans=0;
for(int i=1;i<=n;i++)G[i].clear();
//for(int i=1;i<=n;i++)dp[i]=0;
//init
cin>>n>>k;
int u,v;
for(int i=1;i<n;i++){
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
if(dfs(0,1))++ans;//根节点没父节点,根节点可以随便取 dp[根节点]+=dfs(0,根节点);
cout<<ans<<endl;//cout<<dp[根节点]<<endl;
}
signed main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t=1;
cin>>t;
while(t--)solve();
}
题外话:趁着我们学校的大佬去打澳门区域赛,我们队在这场比赛中成为这场比赛中我们学校排名最靠前的队,窃喜。