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都加入了集合

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);

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章