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

题目链接

题意:

给你一棵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都加入了集合

2018-2019 ACM-ICPC Southeastern European Regional (SEERC 2018) C Tree(level 2)(树的直径)(4种解法)_第1张图片

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 
using namespace std;
const int N = 100 + 5;
vector 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 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

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

 

假定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 
#define maxn 105
using namespace std;
vectorvec[maxn];
int vis[maxn];//用vis数组去区分点的不同的集合
int a[maxn];
queueque;
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>1;
        if(check(mid)) r=mid;
        else l=mid+1;
        //cout<

这里再将一个树形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

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

3.如果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上挺常见的

2018-2019 ACM-ICPC Southeastern European Regional (SEERC 2018) C Tree(level 2)(树的直径)(4种解法)_第3张图片

代码来源

#include 
#define ll long long
using namespace std;
const int MAXN = 300;
int c[MAXN];
int dp[MAXN][MAXN];
vector edge[MAXN];
vector 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= 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>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<

 

 

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

#include 
#include 
#include 
#include 
#include 
using namespace std;

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


vector ee[N];
int dep[N];
int pos[N],Log[N<<2],ST[N<<2][25]; //pos[i]:i第一次出现的位置,ST[i][j]在欧拉序[i,i+(1<> 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>1;
		if(check(mid))
			ans=mid,r=mid;
		else 
			l=mid+1;
	}
	if(check(l)) ans=l;
	printf("%d\n",ans);

}

 

你可能感兴趣的:(树,BFS,dfs,图论,动态规划)