POJ1741 求距离小于K的点对问题
在做这题时网上搜索到很多版本代码,但是都没有注释或者没什么解释,这里稍作解释下解题思路以及放一篇注释版本(注释的比较随意,只想看代码的话直接滑倒最后QAQ!!,如有问题,敬请指出)
思路读懂代码后还是比较简单的:
第一步求整树的重心(Query_size;Query_root)
其实我觉得是一个无向图,这个图的结点是以邻接表的形式表示结点间的关系,add_edge,具体操作其实就是:当前节点指向头节点的后继节点,头节点只想当前节点,每次都会新开一条“路径”e[i],具体可以将节点的变化过程printf出来仔细思考。
第二步根据每个点到当前树重心的距离 判断是否满足小于K,若小于则记录数量(Query_dis;cal)
第三步根据每个点到当前树重心距离+d(当前重心节点与上个重心节点的距离)是否小于K,如果满足则为重复节点对数,减去。
第四步指向当前树重心结点的后继节点,若无后继节点,则结束当前递归过程回退到父节点(循环指向后继节点的)过程
求子递归树重心中为防止回退到前继节点这是vis来判断,因为求当前树重心需要将父节点与当前节点的连接断开,以免计算时将无需计算的节点包含进去致使计算树重心出现错误。
第五步返回第一步
有个在求节点对的计算思路的转换
假设v,w是当前满足的点对,s是当前根节点,那么图上所示即为三种点对的可能状态,第三种w与s位置相同。显然过程中在计算符合要求当前节点对会出现重复计算的可能,即某一些点对,对多个树重心的距离之和满足小于K,所以此时我们在下步计算中将这些重复的节点对找出来,然后减去就行了,具体体现在以下代码段中。
for (int i = head[rt]; i != -1; i = e[i].next){
int v = e[i].to, w = e[i].w;
if (vis[v]) continue;
//在同一棵子树上,且符合条件的顶点。
ans -= cal(v, w);//减去可能的重复计算结点对数
//以v为根继续计算符合条件的点对
dfs(v, rt);
}
最终按照深度遍历的方法对每个结点都求树重心计算并保存满足要求(距离小于K)节点对,然后计算并减去重复结点对,最终得出目标数量
具体的注释版代码如下:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
typedef long long LL;
const int maxn = 2e4 + 5;
const int INF = 1e9 + 7;
int cnt = 0; //当前结点
int head[maxn << 1]; //头节点
int sonnum[maxn << 1]; //以当前结点为根节点的子树的总结点数
int sonmax[maxn << 1]; //Max(以当前节点的孩子结点为根节点的子树中总结点数)
int mi; //当前最优解 min (max( 孩子结点中总结点树最大,n-当前结点总结点数))
int pos; //遍历结果保存树的重心
int N, K; //N节点数 K小于K的距离
int sum = 0;
bool vis[maxn << 1];//子递归阻止退回上一棵树的判断依据
LL ans;
vector<int>dis;
// head 头结点数组 下标号为当前结点号,存储的数据是 存储有下一个结点信息的e的标号
// 如head[1] = 5 e[5].to = 4 说明 1->5 1是5的双亲结点 5是1的一个孩子结点
// 三元组 e.to:当前指向结点号 e.next当前结点的后继结点的e数组的标号 e.w距离
/*例题邻接表
1 -> 4 -> 3 -> 2 -> -1
2 -> 1 -> -1
3 -> 5 -> 1 -> -1
4 -> 1 -> -1
5 -> 3 -> -1
*/
struct Edge{
int next, to, w; //
}e[maxn << 1];
void add_edge(int u, int v, int w){
e[++cnt].to = v;
e[cnt].w = w;
e[cnt].next = head[u];
head[u] = cnt;
}
//初始化 head 头结点数组 置为-1 相当于NULL vis数组
void Init(){
ans = 0; cnt = 0;
for (int i = 0; i < maxn; i++){
head[i] = -1; sonnum[i] = sonmax[i] = 0;
vis[i] = false;
}
}
//求当前树的子树大小
void Query_size(int root, int pre){
sum++;//存树的大小
sonnum[root] = 1; sonmax[root] = 0;//注意初始化
for (int i = head[root]; i != -1; i = e[i].next){
int v = e[i].to;
if (vis[v] || v == pre) continue;
Query_size(v, root);
sonnum[root] += sonnum[v];//子树节点有多少个
sonmax[root] = max(sonmax[root], sonnum[v]);//最大的子树节点个数
}
}
//深度优先的遍历 每次遍历直至叶子结点 操作后回溯前一个结点 后序遍历 最终更节电
void Query_root(int root, int pre, int sum){//求当前树的重心
for (int i = head[root]; i != -1; i = e[i].next){//遍历
int v = e[i].to; //当前结点标号
if (vis[v] || v == pre) //防止循环进入结点
continue;
Query_root(v, root, sum);
}
int ma = max(sonmax[root], sum - sonnum[root]); //树重心的约束条件
if (mi > ma){
mi = ma; pos = root;
}
}
/*
计算程序 Query_dis() cal()
*/
//深度优先遍历
void Query_dis(int root, int pre, int d){
dis.push_back(d);
for (int i = head[root]; i != -1; i = e[i].next){
int v = e[i].to, w = e[i].w;
if (vis[v] || v == pre) continue;
Query_dis(v, root, d + w);
}
}
int cal(int root, int d){
int ret = 0;
dis.clear();//存所有子节点到本身的距离
Query_dis(root, 0, d);
sort(dis.begin(), dis.end()); //从小到大排序
int i = 0, j = dis.size() - 1;
while (i < j){
while (i<j && dis[i] + dis[j]>K) j--;
//结点i到结点i+1,i+2...j之间所有的点均满足最小点对的要求
ret += j - i;
//找下一个起点开始的点对
i++;
}
return ret;
}
void dfs(int root, int pre){
sum = 0;
mi = INF;
Query_size(root, pre);
Query_root(root, pre, sum);//
int rt = pos;//pos为找到的重心
//任意两个点之间的满足条件的点对
ans += cal(rt, 0);
vis[rt] = true;//一定要标记 否则会往回走
for (int i = head[rt]; i != -1; i = e[i].next){
int v = e[i].to, w = e[i].w;
if (vis[v]) continue;
//在同一棵子树上,且符合条件的顶点。
ans -= cal(v, w);//减去可能的重复计算结点对数
//以v为根继续计算符合条件的点对
dfs(v, rt);
}
}
int main(){
while (scanf("%d%d", &N, &K) != EOF){
Init();
if (N == 0 && K == 0) break;
for (int i = 1; i < N; i++){
int u, v, w; scanf("%d%d%d", &u, &v, &w);
add_edge(u, v, w);
add_edge(v, u, w);
}
dfs(1, 0);
printf("%lld\n", ans);
}
return 0;
}
如果讲的有问题,敬请指正,觉得讲的还不错的话,看在博主码了这么多字的份上给个三连呗QAQ!!。