题意:
给你一棵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);
}