2018-2019 ACM-ICPC Southeastern European Regional (SEERC 2018) C Tree(level 2)(树的直径)(4种解法)

本文介绍了ACM-ICPC竞赛中关于树的直径问题的多种解法,包括使用ST的LCA求两点距离、二分搜索结合最大团验证、BFS层次遍历和DFS深度搜索。文章详细解析了每种方法的思路,并提供了相关证明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接

题意:

给你一棵n个点的树(n<=100),每一个点有白/黑色,让你选m个黑色的点,

使得你选的这m个点的集合里最远的两个点的距离最小

解析:

这道题我训练的时候是用st的LCA求两点距离+二分+最大团验证来做的,代码有167行

比赛的时候...估计得写将近1个小时,然后还被自己LCA模板上的一个数组大小卡了半个小时...

这道题赛后看了大佬们的代码,大多都是和树的直径联系在一起的。

可以看一下树的直径及其证明。

里面有一个很重要的性质,就是树上一个点x最远能到达的点一定是直径的一个端点

这道题做法很多,首先一个比较简单版本的就是枚举任意两个点x,y,记录他们的距离为最长距离res

然后把剩余的点k加进来,如果dis[x][k]<=res&&dis[k][y]<=res,那么这个点就是可以加入的

如果最后的点数>=m,那么对答案进行更新

这里为什么点k满足dis[x][k]<=res&&dis[k][y]<=res就可以加入进来,保证k与集合里面的其他点的距离都<=res?

那么下面是证明

 

假定我们枚举的边是st,然后x,y都加入了集合

su=编号1,uv=编号5,vt=编号2,ux=编号4,vy=编号3

那么x,y加入集合条件是1+4<=1+5+2,  4+5+2<=1+5+2

=>4<5+2 && 4<=1

同理3<=5+1 &&  3<=2

那么我们证明4+3+5的长度

4+3+5(xy)<= 1+3+2(st)

那么就满足了条件了

所以这个思想得到的一个结论是

一条树链xy的长度为p,,如果两个点s,t都满足dis[s/t][x]<=p&&dis[s/t][y]<=p

那么dis[s][t]一定满足<=p

代码来源于Engineering Drawing

#include <bits/stdc++.h>
using namespace std;
const int N = 100 + 5;
vector<int> G[N];
int dis[N][N], level[N], col[N], n, m;
void addedge(int u, int v) {
    G[u].push_back(v);
    G[v].push_back(u);
}
void bfs(int s) {
    memset(level, -1, sizeof level);
    queue<int> q;
    level[s] = 0;
    q.push(s);
    while(!q.empty()) {
        int u = q.front(); q.pop();
        for(int v : G[u])
            if(level[v] == -1) {
                level[v] = level[u] + 1;
                q.push(v);
            }
    }
    for(int i = 1; i <= n; i++)
        dis[s][i] = level[i];
}
int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> col[i];
    for(int i = 1; i <= n - 1; i++) {
        int u, v; cin >> u >> v;
        addedge(u, v);
    }
    for(int i = 1; i <= n; i++)
        if(col[i]) bfs(i);
    int ans = 1000;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(col[i] && col[j]) {
                int cnt = 0;
                for(int k = 1; k <= n; k++)
                    if(col[k] && max(dis[i][k], dis[j][k]) <= dis[i][j]) cnt++;
                if(cnt >= m) ans = min(ans, dis[i][j]);
            }
    cout << ans << endl;
}

另外一种是来源于一个博客上的

先二分出一个最大距离k

他的思路就是边bfs边dfs,用bfs层次遍历

然后用bfs遍历过的点的vis[]标记重新建树

假定一开始我们以1为根,那么bfs层次遍历的时候遍历到x

x一定是距离1最远的点,距离为x的层数

那么x也一定是bfs层次遍历新建的树的直径的一个端点(叶子节点),

那么我们只需要从这个端点出发dfs(假定这个点的深度为0),深度<=k的黑点有多少

如果有>=m个,那么就返回1,否则返回0

 

假定bfs起点是t,现在bfs遍历到s.上面是遍历到s时新建的bfs层次遍历树

su=编号2,vu=编号3,xu=编号1,vy=编号4,vt=编号5

那么从s开始dfs,假定x,y都是可以选入集合的点,即sx=1+2<=k,sy=2+3+4<=k

那么怎么保证1+3+4<=k?

有bfs层次树的性质是5+3+1<=5+3+2,  5+4<=5+3+2

=>  1<=2  ,  4<=3+2

那么1+3+4(xy)<=2+3+4(sy)<=k

这个思想的结论是

从树上深度最深的点/直径的一个端点(保证该点所在的层数都>=其他点),记作s,出发dfs形成的dfs树。只要保证该dfs树上的点y到s的距离<=k(即y在s的dfs树上的深度<=k,等价于这棵dfs树的高度==k),那么这棵dfs树上任意两点的距离都<=k

#include <bits/stdc++.h>
#define maxn 105
using namespace std;
vector<int>vec[maxn];
int vis[maxn];//用vis数组去区分点的不同的集合
int a[maxn];
queue<int>que;
int n,m;
int ans=0;
int dfs(int now,int fa,int all,int dis){
    int res=a[now];
    if(dis==all) return res;
    for(auto &it:vec[now]){
        if(!vis[it]||it==fa) continue;
        res+=dfs(it,now,all,dis+1);
    }
    return res;
}
bool check(int k){//二分的check,本质上为一个bfs
    memset(vis,0,sizeof(vis));
    while(!que.empty()) que.pop();
    que.push(1);
    while(!que.empty()){//bfs选取部分点集
        int now=que.front();
        que.pop();
        vis[now]=1;
        int tmp=dfs(now,0,k,0);//通过dfs获取这个集合的黑点的个数
        if(tmp>=m) return 1;
        for(auto &it:vec[now]){
            if(vis[it]) continue;
            que.push(it);
        }
    }
    return 0;
}
int main()
{
    //freopen("in.txt","r",stdin);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=0;i<n-1;i++){
        int from,to;
        scanf("%d%d",&from,&to);
        vec[from].push_back(to);
        vec[to].push_back(from);
    }
    int l=0,r=n;
    while(l<r){
        int mid=(l+r)>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
        //cout<<l<<" "<<r<<endl;
    }
    cout<<r<<endl;
}

这里再将一个树形dp的版本,因为我看也有很多人是用这个过的。

二分答案的时候check用树形dp

dp[i][j]表示以i为根,到i的距离<=j的黑色节点的个数,同时保持任意两个点的距离<=md

其实就是维护一棵以i为根的树,这棵树任意两点的距离<=md,并使这棵树的黑色节点最多,

即一棵以i为根,树的高度<=j,且树上任意两点距离<=md的节点最多的黑树

下面是转移。我们得到dp[i][j]通过三种途径转移。

1.从dp[i][j-1]转移 

2.如果j-1<md-1-j,即2*j<md,那么dp[i][j]从dp[v][j-1]+除v以外的孩子节点的最大能达到的黑树的高度j-1转移过来的,

即从\sum _{v\epsilon son(i)} dp[v][j-1]转移。这样求解一个原因也源于dp[i][j]是高度<=j的最优情况,所以永远有dp[i][j]>=dp[i][k] k<j

3.如果md-1-j<j-1,那么对于v∈son(i),dp[v][j-1]状态就不能加其他孩子的j-1状态,而是md-1-j的状态,这样使得任意两点的

距离<=md

max{dp[v][j-1]-dp[v][md-1-j]+\sum _{u\epsilon son(i)} dp[u][md-1-j]}

上面都-1是因为孩子节点到父节点还有1的距离要加

那么dp[i][j]取三者之中的最大值就可以了。这个套路其实在树形dp上挺常见的

代码来源

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN = 300;
int c[MAXN];
int dp[MAXN][MAXN];
vector<int> edge[MAXN];
vector<int> in;
int n,m;
void dfs(int u,int p,int mid)
{
    for(int v : edge[u])
    {
        if(v == p) continue;
        dfs(v,u,mid);
    }
    if(c[u]) dp[u][0] = 1;
    for(int i = 1;i<=mid;i++)
    {
        int mx = min(mid-i-1,i-1),sum = 0;
        dp[u][i] = max(dp[u][i],dp[u][i-1]);
        if(mx >= 0)
        {
            for(int v : edge[u])
            {
                if(v != p) sum += dp[v][mx];
            }
        }
        for(int v : edge[u])
        {
            if(v != p)
            {
                int tmp = dp[v][i-1];
                if(mx>=0) tmp += sum - dp[v][mx];
                dp[u][i] = max(dp[u][i],c[u]+tmp);
            }
        }

    }
}
bool check(int mid)
{
    //for(int i= 0;i<MAXN;i++) for(int j  =0;j<MAXN;j++) dp[i][j] = 0;
    memset(dp,0,sizeof(dp));
    dfs(1,-1,mid);
    for(int i = 1;i<=n;i++)
    {
        if(dp[i][mid] >= m) return 1;
    }
    return 0;
}
int main()
{

    cin>>n>>m;
    for(int i = 1;i<=n;i++)
    {
        cin>>c[i];
    }
    for(int i = 0;i<n-1;i++)
    {
        int u,v;
        cin>>u>>v;
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    int l = 0,r = n,ans = 0;
    while(l <=r )
    {
        int mid =(l+r)>>1;
        if(check(mid))
        {
            ans = mid;
            r = mid-1;
        }
        else
        {
            l = mid+1;
        }
    }
    cout<<ans<<endl;
    return 0;
}

 

 

最后放一个st的LCA求两点距离+二分+最大团,167行的代码...

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

const int N =200+10;
typedef long long ll;
const int MOD = 1e9+7;


vector<int> ee[N];
int dep[N];
int pos[N],Log[N<<2],ST[N<<2][25]; //pos[i]:i第一次出现的位置,ST[i][j]在欧拉序[i,i+(1<<j))中dep最小的点
int tot;
int fa[N][25]; //fa[i][j]记录第i个节点的第(1<<j)个父亲,(非必要)
int col[N];
int dis[N][N];
int mp[N][N];


int Min(int x,int y) { 
	return dep[x] < dep[y] ? x : y;
}


void dfs(int u)
{
	ST[++tot][0] = u; pos[u] = tot;
	for (int i = 0; i<ee[u].size(); i ++) {
		int v = ee[u][i];
		if (v == fa[u][0]) continue;
		fa[v][0] = u, dep[v] = dep[u] + 1;
		
		dfs(v);
		ST[++tot][0] = u;//!
	}

}

void init(int n)
{
	tot=0;
	dfs(1);
	Log[0] = -1;
	for (int i = 1; i <= tot; ++i) Log[i] = Log[i >> 1] + 1;
	for (int j = 1; j <= Log[n]; ++j) 
		for (int i = 1; i <= n; ++i) fa[i][j] = fa[fa[i][j - 1]][j - 1];
	for (int j = 1; j <= Log[tot]; ++j) 
		for (int i = 1; i <= tot; ++i) ST[i][j] = Min(ST[i][j - 1], ST[i + (1 << (j - 1))][j - 1]);

}

int LCA(int u,int v) {
	if (u == v) return u;
	u = pos[u], v = pos[v];
	if (u > v) swap(u, v); 
	//u ++;   //?
	int k = Log[v - u + 1];
	return Min(ST[u][k], ST[v - (1 << k) + 1][k]);
}


int cal_dis(int u,int v)
{
	int f=LCA(u,v);
	return dep[u]-dep[f]+dep[v]-dep[f];
}
int n,m;
int all[N][N],some[N][N],none[N][N];

int BKdfs(int depth,int an,int sn,int nn)
{
	int i,j,u,v;
	if(an>=m) return 1;
	if(sn==0&&nn==0)      //得到极大团,最大团是极大团里面顶点数最多的一个
	{
		if(an>=m) return 1;
		else return 0;
	}
	u=some[depth][0];   //将第0个点拿来剪枝
	for(i=0;i<sn;i++)
	{
		v=some[depth][i];
		if(mp[u][v])continue;     //剪枝,若u与v相邻,u已经算过它的极大团,那么这个极大团一定包含v,所以也是v的极大团,所以是重复的情况
		for(j=0;j<an;j++)all[depth+1][j]=all[depth][j];     //为下一层深度更新数组
		all[depth+1][an]=v;
		int ssn=0,nnn=0;
		for(j=0;j<sn;j++)if(mp[v][some[depth][j]])some[depth+1][ssn++]=some[depth][j]; 
		//none,some里面的下一层元素必须与当前深度加入all的点v邻接
		for(j=0;j<nn;j++)if(mp[v][none[depth][j]])none[depth+1][nnn++]=none[depth][j];
		if(BKdfs(depth+1,an+1,ssn,nnn))return 1;
		//将v从some中取出来,放入none
		some[depth][i]=0;
		none[depth][nn++]=v;    //将v从all里淘汰,即尝试其他当前深度与v非邻接的点
	}
	return 0;
}


int check(int md)
{
	for(int i=1;i<=n;i++)
	{
		for(int j=i+1;j<=n;j++)
		{
			if(col[i]&&col[j]&&dis[i][j]<=md)
				mp[i][j]=mp[j][i]=1;
			else 
				mp[i][j]=mp[j][i]=0;
		}
	}
	for(int i=0;i<n;i++)some[1][i]=i+1;  //点的范围[1,n]
	if(BKdfs(1,0,n,0)) return 1;
	else return 0;
	

}



int main()
{
	scanf("%d%d",&n,&m);
	
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&col[i]);
	}
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		ee[u].push_back(v);
		ee[v].push_back(u);
	}
	init(n);
	for(int i=1;i<=n;i++)
	{
		if(!col[i]) continue;
		for(int j=i+1;j<=n;j++)
		{
			if(!col[j]) continue;
			dis[i][j]=dis[j][i]=cal_dis(i,j);
		}
	}
	int l=0;
	int r=n;
	int ans=0;
	while(l<r)
	{
		int mid=(l+r)>>1;
		if(check(mid))
			ans=mid,r=mid;
		else 
			l=mid+1;
	}
	if(check(l)) ans=l;
	printf("%d\n",ans);

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值