题意:
给定你
n
n
n个节点,分别涂有黑色或者白色,
0
0
0表示白色,
1
1
1表示黑色。然后给定你一个
m
m
m,问你能找到
m
m
m个黑色节点使得他们之间任意两点间的距离的最大值最下为多少,输出这个最小值。
输入保证一定存在答案,保证
n
n
n个节点构成一棵树。
思路:
首先应该知道树的直径的定义:
a. 树上任意两点间的简单路径的最大值
b.若树的直径的端点为是 s , t s,t s,t,则任意点 x x x到其余点的最远点一定在 s s s或者 t t t处
c.若直径端点为 s , t s,t s,t,两点 x , y , d i s [ s ] [ x ] ≤ d i s [ s ] [ t ] & & d i s [ x ] [ t ] ≤ d i s [ s ] [ t ] x,y,dis[s][x] \leq dis[s][t] \&\&dis[x][t] \leq dis[s][t] x,y,dis[s][x]≤dis[s][t]&&dis[x][t]≤dis[s][t],同理 d i s [ s ] [ y ] ≤ d i s [ s ] [ t ] & & d i s [ y ] [ t ] ≤ d i s [ s ] [ t ] dis[s][y] \leq dis[s][t] \&\&dis[y][t] \leq dis[s][t] dis[s][y]≤dis[s][t]&&dis[y][t]≤dis[s][t],则 d i s [ x ] [ y ] ≤ d i s [ s ] [ t ] dis[x][y] \leq dis[s][t] dis[x][y]≤dis[s][t]。
第三个定理呢画画图就可证明了。
解法一:
有了上面三个定理支持,我们就可以枚举两点
i
,
j
i,j
i,j之间的距离并假设其为树的直径的端点,然后再去枚举其他点
k
k
k,如果这个点
k
k
k到
i
,
j
i,j
i,j的距离满足第三个式子,那么就把它加入当前点的集合,如果最后的点的集合内的黑色点的个数超过了
m
m
m,那么就更新一遍答案,最后求最小值即可。
#include <bits/stdc++.h>
using namespace std;
#define pb emplace_back
#define MP make_pair
#define pii pair<int,int>
#define pll pair<ll,ll>
#define lson rt<<1
#define rson rt<<1|1
#define CLOSE std::ios::sync_with_stdio(false)
#define sz(x) (int)(x).size()
typedef long long ll;
typedef double db;
const int INF = 0x3f3f3f3f;
const db eps = 1e-6;
const int N = 100 + 10;
int col[N],n,m;
int head[N],tot;
struct edge {
int next,to;
}edge[N<<1];
void addedge(int u,int v) {
edge[++tot].to = v,edge[tot].next = head[u],head[u] = tot;
edge[++tot].to = u,edge[tot].next = head[v],head[v] = tot;
}
//要用到一个类似于树的直径的性质
int dis[N][N];
void dfs(int rt,int u,int fa,int d) {
dis[u][rt] = d;
for(int i = head[u];i;i = edge[i].next) {
int v = edge[i].to;
if(v == fa) continue;
dfs(rt,v,u,d+1);
}
}
int ans;
void solve() {
int cnt;
for(int i = 1;i <= n;i ++) {
for(int j = 1;j <= n;j ++) {//假设枚举的这两个点间的距离就是最远距离
if(j == i) continue;
if(col[j] && col[i]) {
// cout << i << ' ' << j << ' ' << dis[i][j] << '\n';
if(j != i) cnt = 2;
else cnt = 1;
for(int k = 1;k <= n;k ++) {
if(k == i || k == j) continue;
if(col[k] && dis[i][k] <= dis[i][j] && dis[k][j] <= dis[i][j]) {//这个判断保证了假如进来的k不破坏这个最长距离
cnt++;
}
}
if(cnt >= m) {
ans = min(ans,dis[i][j]);
}
}
}
}
}
int main() {
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i ++) {
scanf("%d",&col[i]);
}
int a,b;
for(int i = 1;i < n;i ++) {
scanf("%d%d",&a,&b);
addedge(a,b);
}
for(int i = 1;i <= n;i ++) {
if(col[i]) dfs(i,i,0,0);
}
ans = INF;
solve();
printf("%d\n",ans);
return 0;
}
解法二:
看到最大值最小我们可以想到,二分解决,我们去二分这个最大值 x x x,很明显加入我们能保证当前的点组成的一棵树的直径的值等于 x x x,那么这棵树终点的任意两点距离一定比 x x x小,此时我们再统计这棵树的黑色节点个数,与 m m m比较,即可得知是否合法。
那么怎么确定一棵直径为 x x x的树呢,这里要设计树的直径怎么求,其中一种方法就是,随机选取一个点 o o o,然后 b f s bfs bfs求出这个点能到达的最远点 s s s,然后再从 s s s出发做一次 b f s bfs bfs求出到达的最远点 t t t,此时 s − t s-t s−t即为树的直径。
这样我们对于每次二分的最大值 x x x,随机选一个点开始做 b f s bfs bfs,然后每走到一个点,我们就可以假设这个点为当前能到达的最远点,此时再去对之前遍历过的节点做 b f s bfs bfs,注意一定是前面遍历过的点,否则就无法保证这个点是最远点了,然后如果某一层到当前的点的距离大于 x x x后就退出,此时就找到了,当前这棵树以 x x x为直径能到的最远点,此时遍历过的点都处于一个集合中,并且任意两点的距离不会大于 x x x,然后就看集合中的黑色节点大于 m m m嘛,大于就是合法。
#include <bits/stdc++.h>
using namespace std;
#define pb emplace_back
#define MP make_pair
#define pii pair<int,int>
#define pll pair<ll,ll>
#define lson rt<<1
#define rson rt<<1|1
#define CLOSE std::ios::sync_with_stdio(false)
#define sz(x) (int)(x).size()
typedef long long ll;
typedef double db;
const int INF = 0x3f3f3f3f;
const db eps = 1e-6;
const int N = 100 + 10;
int n,m,col[N];
std::vector<int>G[N];
void addedge(int u,int v) {
G[u].pb(v);
G[v].pb(u);
}
int vis[N],vis2[N],dep[N];
int bfs(int s,int aim) {
memset(dep,0,sizeof(dep));
memset(vis2,0,sizeof(vis2));
int cnt = 0;
queue<int>q;
q.push(s);
vis2[s] = 1;
while(!q.empty()) {
int u = q.front();
q.pop();
if(dep[u] > aim) break;
if(col[u]) cnt++;
for(int i = 0;i < sz(G[u]);i ++) {
int v = G[u][i];
if(vis2[v] || !vis[v]) continue;
vis2[v] = 1;
dep[v] = dep[u] + 1;
q.push(v);
}
}
return cnt;
}
bool check(int x) {
memset(vis,0,sizeof(vis));
queue<int>q;
vis[1] = 1;
q.push(1);
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = 0;i < sz(G[u]);i ++) {
int v = G[u][i];
if(vis[v]) continue;
// cout << u << ' ' << v << '\n';
int tt = bfs(v,x);
// cout << "*****" << v << ' ' << tt << ' ' << x << '\n';
if(tt >= m) return true;
vis[v] = 1;
q.push(v);
}
}
return false;
}
int main() {
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i ++) scanf("%d",&col[i]);
int a,b;
for(int i = 1;i < n;i ++) {
scanf("%d%d",&a,&b);
addedge(a,b);
}
if(n == 1) {
return printf("0\n"),0;
}
//二分最长距离
//但其实还是利用了树的直径那个方法和思想 对于每个点相当于重建一棵树 保证这棵树的直径小于等于k
//那么这棵树之间的任意点的距离是不会超过k的
int l = 0,r = n,ans;
while(l <= r) {
int mid = (l + r) >> 1;
if(check(mid)) {
ans = mid;
r = mid - 1;
}
else l = mid + 1;
}
printf("%d\n",ans);
return 0;
}
/*
6 3
1 1 0 1 1 1
1 2
1 3
1 4
3 5
3 6
9 4
1 0 1 0 1 0 0 1 1
1 2
2 4
2 3
4 5
1 6
6 7
6 8
7 9
1 1
1
*/