題意:
給你一棵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轉移過來的,
即從轉移。這樣求解一個原因也源於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{}
上面都-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);
}