原题
给定一个有 N 个点(编号 0,1,…,N−1)的树,每条边都有一个权值(不超过 1000)。
树上两个节点 x 与 y 之间的路径长度就是路径上各条边的权值之和。
求长度不超过 K 的路径有多少条。
输入格式
输入包含多组测试用例。
每组测试用例的第一行包含两个整数 N 和 K。
接下来 N−1 行,每行包含三个整数 u,v,l,表示节点 u 与 v 之间存在一条边,且边的权值为 l。
当输入用例 N=0,K=0 时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果。
每个结果占一行。
数据范围
1≤N≤104,
1≤K≤5×106,
1≤l≤103
输入样例:
5 4
0 1 3
0 2 1
0 3 2
2 4 1
0 0
输出样例:
8
首先要知道几个基础概念和性质:
树的重心:将树的某一个点删除,生成许多连通块,能使得分出的所有连通块中点数最大值最小的点就是树的重心
性质:将树的重心删除后生成的所有连通块中,点数最多的连通块的点数<=n/2 n为该树的节点树
===========================
证明:使用反证法,假设生成的最大连通块的点数为N,且N>n/2
那么如果我们将与当前重心点相连且属于最大连通块的那个点当作重心删除
并恢复原本的重心
那么删除新重心生成的最大连通块的点数为N-1,由此即可证明最大连通块的点<=n/2
============================
对于题目中问询有多少路径<=K,我们可以把路径分成三类
第一类:路径两个端点在同一子树内部(用递归去求)
第二类:路径两个端点不在同一子树
先求子树中每一个点到重心的距离,接下来就是从所有点中任选两个点距离相加来判断是否小于K,但如果这样选就会出现一些不合法情况(选择的两个点在同一个子树) .所以我们可以用容斥原理 减去两个点在同一个树的情况的数量(如果直接求从不同树中选点的满足的情况很麻烦)
第三类:其中有一个点在重心(很好处理,只需要从重心向子树遍历,求每个点的距离即可)
最后一个需要解决的问题:给出一堆数,任选两个之和小于K的总方案数
我们可以把这些数排序,然后用二分的方法,枚举其中一个数,二分另一个数.这样就能把O(n^2)降成O(nlogn)
也可以排序后用双指针算法
以下是代码,看再多遍不如跟着敲一遍
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010, M = 2 * N;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
bool st[N];//表明某点有没有被删掉
int p[N];//当前重心离所有子节点的距离
int q[N];//当前子节点的距离
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int get_size(int u, int fa) { //求子树大小
if (st[u])
return 0;
int res = 1;
for (int i = h[u]; ~i; i = ne[i]) {
if (e[i] != fa) {
res += get_size(e[i], u);
}
}
return res;
}
// 子树大小
int get_wc(int u, int fa, int tot, int &wc) { //求重心
//只要把该点删后,最大连通块小于n/2,满足时间复杂度即可,不必求真正的重心
if (st[u])
return 0;
int sum = 1; //当前整个树的节点总数
int ms = 0; //把u点删掉后最大连通块的点数
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j == fa)
continue;
int t = get_wc(j, u, tot, wc);
ms = max(ms, t);
sum += t;
}
ms = max(ms, tot - sum); //还要考虑其父节点所在连通块的大小
if (ms <= tot / 2)
wc = u;
return sum;
}
void get_dist(int u, int fa, int dist, int &qt) {
if (st[u])
return ;
q[qt++] = dist;
for (int i = h[u]; ~i; i = ne[i]) {
if (e[i] != fa) {
get_dist(e[i], u, dist + w[i], qt);
}
}
}
int get(int a[], int k) { //数组,大小
sort(a, a + k);
int res = 0;
for (int i = k - 1, j = -1; i >= 0; i--) { //双指针算法求方案数
while (j + 1 < i && a[j + 1] + a[i] <= m)
j++;
j = min(j, i - 1); //j不能大于i
res += j + 1; //方案数就是从0到j
}
return res;
}
int calc(int u) { //处理u所在的树
if (st[u])
return 0;
int res = 0; //表示当前子树内,有多少满足要求的数对
get_wc(u, -1, get_size(u, -1), u); //求重心
st[u] = true;
//归并部分
int pt = 0; //p数组的大小
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i], qt = 0; //q数组的大小,用法类似idx
get_dist(j, -1, w[i], qt); //遍历j所在子树里面,所有点到重心的距离
// ↑第一条边的距离
res -= get(q, qt); //减去非法情况 get是用于求给一堆数,求其中任意两个数小于K的方案数
for (int k = 0; k < qt; k++) { //将部分并入总体
if (q[k] <= m)
res++;//计算第三类情况,一个点是重心
p[pt++] = q[k];
}
}
res += get(p, pt); //(包含非法情况)的种方案数,但非法方案已经被提前减去
for (int i = h[u]; ~i; i = ne[i]) {
res += calc(e[i]);
}
return res;
}
int main() {
while (cin >> n >> m) {
if (n == 0 && m == 0)
break;
memset(h, -1, sizeof h);
memset(st, false, sizeof st);
idx = 0;
for (int i = 0; i < n - 1; i++) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
cout << calc(0) << endl;
}
return 0;
}
题目二
给定一棵 N 个节点的树,每条边带有一个权值。
求一条简单路径,路径上各条边的权值和等于 K,且路径包含的边的数量最少。
输入格式
第一行两个整数 N,K。
第 2∼N 行每行三个整数 x,y,z,表示一条无向边的两个端点 x,y 和权值 z,点的编号从 0 开始。
输出格式
输出一个整数,表示最少边数量。
如果不存在满足要求的路径,输出 −1。
数据范围
1≤N≤2×105,
1≤K≤106,
0≤z≤106
输入样例:
4 3
0 1 1
1 2 2
1 3 4
输出样例:
2
与上一题思考方式相同,将边分为三种:在同一个子树中,在不同子树中。一个端点是重心
为了实现题目中的要求,我们设一个数组F[N]
F[i]表示:距离当前重心距离为i的所有点中,到达当前重心所需经过的边最少的那个点。
由于这个数组是对于当前重心而言的,所以在我们进行分治时要及时清理数据。
具体算法流程是:
- 找到整张图的重心
- 搜索与该重心相连的连通块,获取当前连通块中与每个点与重心的距离和边距离数
- 处理第三种边:直接判断某点与重心的距离是否等于K即可,如果成立就尝试更新答案ans=min(ans,该点的边距)
- 处理第二种边:ans=min(ans,F[K-当前点的距离]+当前点的边距)
- 处理完边后,更新F数组:F[当前点距离]=min(F[当前点距离],当前点边距)
- 接着我们就要分治各个连通块,由于连通块的重心与当前重心不同,所以我们要清理掉F数组,但直接用memset(F,0x3f,sizeof F)会导致超时.我们用P[N]储存当前搜索到的点,遍历搜索到的点,一一进行还原
- 分治结束,算法结束
以下为代码,有注释,建议跟着敲一遍
#include <bits/stdc++.h>
#define x first
#define y second
using namespace std;
const int N = 200010, M = 2 * N, S = 1000010, INF = 0x3f3f3f3f;
int n, m;
typedef pair<int, int>PII;
int h[N], e[M], w[M], ne[M], idx;
int f[S], ans = INF; //f[i]表示与重心距离为i的点中边数最少的
PII p[N], q[N]; //x距离 y边数
bool st[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int get_size(int u, int fa) {
if (st[u])
return 0;
int res = 1;
for (int i = h[u]; ~i; i = ne[i]) {
if (e[i] != fa) {
res += get_size(e[i], u);
}
}
return res;
}
int get_wc(int u, int fa, int tot, int &wc) {
if (st[u])
return 0;
int sum = 1, ms = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j == fa)
continue;
int t = get_wc(j, u, tot, wc);
ms = max(ms, t);
sum += t;
}
ms = max(ms, tot - sum);
if (ms <= tot / 2) {
wc = u;
}
return sum;
}
void get_dist(int u, int fa, int dist, int cnt, int &qt) {
if (st[u] || dist > m)
return ;//如果距离已经大于m,搜索就没有必要了
q[qt++] = {dist, cnt}; //距离重心的距离/边数
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j == fa)
continue;
get_dist(j, u, dist + w[i], cnt + 1, qt);
}
}
void calc(int u) {
if (st[u])
return ;
get_wc(u, -1, get_size(u, -1), u); //获取重心u
st[u] = true;
//开始归并
int pt = 0; //所有子树点数
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
int qt = 0; //当前子树点数
get_dist(j, -1, w[i], 1, qt); //j所在子树所有点与当前重心的距离
for (int k = 0; k < qt; k++) {
auto &t = q[k]; //当前一个点的距离/边数
if (t.x == m) { //距离刚好满足条件
ans = min(ans, t.y);
}
ans = min(ans, f[m - t.x] + t.y); //f[m-t.x]代表某一个距离重心为t.x的点
//边数为f[m-t.x],再加上这个t的边数
p[pt++] = t; //将其放入
}
for (int k = 0; k < qt; k++) {
auto &t = q[k];
f[t.x] = min(f[t.x], t.y); //用搜的这个子树的点来更新f数组
}
}
for (int i = 0; i < pt; i++) {
f[p[i].x] = INF; //f数组代表的是离u点重心的距离,接下来要递归其他重心,所以要清空
}
for (int i = h[u]; ~i; i = ne[i]) {
calc(e[i]);
}
}
int main() {
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i++) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
memset(f, 0x3f, sizeof f);
calc(0);
if (ans == INF)
ans = -1;
cout << ans << endl;
}