POJ1741 求距离小于K的点对详解(树重心,分治算法,深度优先遍历dfs)

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!!。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值