アルゴリズム分析コースの設計(4)除算と征服の方法を使用して、ツリー内の任意の2点間の距離がK未満の点のペアとパスを見つけます。

免責事項

この記事は個人的な学習ノートですので、注意して参照してください。間違いがある場合は、批判して訂正してください。

参考記事

最初の記事は主に木の重心に注目しています

2番目の記事はこの質問とまったく同じです

https://blog.csdn.net/a_forever_dream/article/details/81778649

https://blog.csdn.net/jackypigpig/article/details/69808594

請求:

(1)疑似コードを使用して、ツリーの重心を見つけるためのアルゴリズムを記述します。

(2)以下のツリーを入力として使用する場合は、上記の問題を解決するための解決プロセスと解決結果を記述します。解決プロセスの主な変数の変更プロセスを記述する必要があります。

(3)問題を解決するためのプログラムを作成し、アルゴリズムの時間の複雑さを分析します。

分析

プロセス全体:

1.木の重心を見つけます。

2.各ポイントから重心までの距離配列を計算します。

3.重心を通過するすべてのポイントペアから、重心サブノードを通過するすべてのポイントペアを引いて、法的なポイント対数を取得します。

4.重心(再帰)の子ノードに対して操作1、2、および3を繰り返します。

1.木の重心を見つけます。

木の重心は、木の質量中心とも呼ばれます。つまり、ツリーのノードの場合、削除後のすべてのサブツリーのノードの最大数(他のノードの削除と比較して)が最小になります。写真が示すように:

ツリーの重心を要求するには、おそらく最初のアイデアは、すべてのノードを1回トラバースし、各ノードを重心として扱い、各サブツリーのノード数を計算してから比較することです。しかし、この暴力的な法律は望ましくありません。同じパスを何度も計算し、ツリー全体を1回スキャンするだけで、各ポイントを重心とするサブツリーノードの最大数を取得できます。ここでは、ツリーのポイント分割と征服の方法を使用します。分割して征服する、それはポストオーダートラバーサルの再帰呼び出しとして理解していますが、再帰は開始点として1つのノードからのみ再帰できます。すべての点から開始するサブツリーノードの最大数を計算するにはどうすればよいですか?ここでも、上記のツリーを例として取り上げます。nを使用して、ツリー全体のノード数を表します。ここで、n = 5です。size[i]を使用して、iをルートとするツリーのノード数を表します。max_child[i]を使用して、iをルートとするサブツリーノードの最大数を表します。minを使用します。 max_child [i]の最小値を更新するには、重心を更新するために使用されます。minの開始値は大きい数値です。

その後のトラバーサルの再帰に従って、最初にポイント4のサイズ[4] = 1を計算でき、赤い部分はn-サイズ[4] = 4ノードです。この分割の理由は、ポイント4には親ノードが1つしかないため、赤い部分は常にその子ツリーとして使用できるためです。size [4]とn-size [4]のサイズを比較すると、ポイント4をルートとするサブツリーノードの最大数はmax_child [4] = 4であり、minはmax_child [4]の4に更新されます。

次に、ポイント4と同様にポイント5のsize [5] = 1を計算します。赤い部分はn-size [5] = 4ノードで、ポイント5をルートとして持つサブツリーノードの最大数max_child [5] = 4、minは変更されていません。

次に、それ自体のノードであるポイント2と、そのサブツリーポイント4および5のサイズを計算します。緑の部分はそのサブツリーであり、size [4] = 1ノード、size [5] = 1ノードであり、赤の部分はn-size [2] = 2ノードであるため、ポイント2はルートmax_child [2] = 2のサブツリーノードの最大数、およびminが2に更新されます。

次に、それ自体の1ノードであるポイント3を計算します。赤い部分はn-size [3] = 4ノードであるため、ポイント3をルートとするサブツリーノードの最大数はmax_child [3] = 4であり、minは変更されません。

次に、それ自体のノードであるポイント1と、そのサブツリーポイント2および3のサイズを計算します。緑の部分は、size [2] = 3ノードとsize [3] = 1ノードの2つのサブツリーです。赤い部分はなくなり、n-size [1] = 0ノードになります。したがって、ポイント1をルートとするサブツリーノードの最大数はmax_child [1] = 3であり、minは変更されません。

minが更新されると、重心も対応するノードに更新されます。ここには書かれていません。コードを見ることができます。

したがって、この再帰の開始時に誰を選択するかは重要ではありません。任意のポイントを選択すると、各ポイントをルートとするサブツリーノードの最大数を計算し、重心を見つけることができます。

木の重心を見つけるためのコードは次のとおりです。

// 全局变量
int n=17; // 所有结点数
int size[n];// 以n为根的树的结点数
int max_child[n];// 以n为根的树的最大子树的结点数
int min;// max_child中最小的那个
int first[n+1],edge[(n-1)*2]// 顶点表,边表
int gravity=0;// 被选为重心的结点

// 传入参数
// start 代表当前结点
// parent 代表当前结点的父结点,这里是为了防止遍历start的子结点的时候把父结点也遍历进去了
void getGravity(int start, int parent)
{
    // size[start]代表当前结点的个数,初始为1是算上本身
    size[start]=1;
    // max_child[start]代表当前结点的最大子树结点数
    max_child[start]=0;
    // 遍历以start结点为起点的所有边(除去连接父结点的边)
    for(int i=first[start]; i; i=edge[i].next)
    {
        // end为当前边的终点(也是start点的子结点)
        int end=edge[i].end;
        // 如果这个终点已经被遍历或者这个终点是父结点,就跳过
        if(visited[end] || end==parent){
            continue;
        }
        // 继续遍历终点的子结点,这里其实就是遍历start的一棵子树的所有结点数
        getGravity(end, start);
        // 遍历完这棵子树的所有结点后,把子树的结点数加起来
        size[start]+=size[end];
        // 如果这颗子树的结点数大于max_child,就更新它
        if(size[end] > max_child[start]){
            max_child[start]=size[end];
        }
    }
    // 上面的循环是用来遍历start的每一棵子树并比较出最大子树结点
    // 接下来就是算“红色部分”,也就是n-size[i]部分的结点数,并比较出最终的最大子树结点
    if(n-size[start] > max_child[start]){
        max_child[start]=n-size[start];
    }
    // 从max_child中比较出最小的,以找出重心
    if(min < max_child[start]){
        min=max_child[x];
        gravity=start;
    }
}

ここでのツリーの保存方法は、最初に頂点テーブルを使用して各ポイントを保存することです。first[i]は、iの番号が付けられたノードに対応する最初のエッジの番号を表します。エッジテーブルエッジの各行は、開始点、終了点、このエッジの重み、および同じ開始点を持つ次のエッジを含むエッジを表します。これらのエッジは無向であるため、エッジはエッジテーブルに2回格納されます。

ツリーノードとエッジを追加するコードは次のとおりです。

int first[n+1],edge[(n-1)*2]// 顶点表,边表
int num=0;
void addNodeAndEdge(int start,int end,int weight)
{
    num++;// 编号,没错,要从1开始
    edge[num].start=start;// 起点
    edge[num].end=end;// 终点
    edge[num].weight=weight;// 权重
    edge[num].next=first[start];// 下一条相同起点的边
    first[start]=num;// 加入顶点
}

2.各ポイントから重心までの距離配列を計算します。

木の重心を選択した後、すべての点からこの重心までの距離(つまり重量)を、同じく再帰的な方法を使用して計算します。これは、あまり説明しなくても、理解しやすいはずです。

int t=0;
// start是传入的点,parent是start的父结点,weight是start和parent连线的权重
// parent在这里是为了防止遍历start的子结点的时候把父结点也遍历进去了
// 因为每个点到start的距离是自上而下地累加,所以传入weight
// 该递归函数的主要作用是返回每个点到重心的距离,所以一开始调用递归函数的时候
// start默认是重心,parent和weight默认是0。
void getDistance(int start, int parent, int weight)
{
    // dis数组保存了每个点到重心的距离(权重),为什么用t来做下标而不是点的编号呢
    // 因为后面的做法只用数点对的个数,不在乎是谁到谁
    // t是从1开始的
    dis[++t]=weight;
    for(int i=first[start]; i; i=edge[i].next)
    {
        int end=edge[i].end;
        // 如果这个终点已经被遍历或者这个终点是父结点,就跳过
        // 这点很重要,因为如果传入的start不是根结点而是根结点的子树的时候
        // 它就不会把根结点再遍历一次
        if(visited[end] || end==parent){
            continue;
        }
        getDistance(end, start, weight+edge[i].weight);
    }
}

このステップでは、dis配列を取得し、各ポイントから重心までの距離をdisに格納します。繰り返しますが、disの添え字はノード番号とは何の関係もありません。次の図に示すように、前の例を見てください。

3.重心を通過するすべてのポイントペアから、重心サブノードを通過するすべてのポイントペアを引いて、法的なポイント対数を取得します。

次に、長さがK未満のパスの数を検討します。私の最初のアイデアは、disでK以下のポイントを見つけることです。そのため、ポイントから重心までの距離がK未満のポイント対数を選択し、重心を通過して距離がK未満のポイント対数を計算します。しかし、2番目の参考記事によると、これは当てはまりません。引用セクションの2番目の参照記事:

(dis配列を取得)後、このサブツリー内の接続されたパスは重心を通過し、答えに寄与します(つまり、距離がk未満のポイントペア(i,j)(i<j))は次のようになります: dis[i]+dis[j] <= K 重心を削除した後、iとj はそうではありません同じUnicomブロック内

ただし、「同じ相互接続ブロックにない」という条件を満たすのは明らかに少し厄介なので、少しトリックがあります。同じ相互接続ブロックにあるかどうかに関係なく、現在のツリーの一致するパスの数を計算してから、重心の子ノードをルートとするサブツリー内のポイントからパスまでの距離(重心を通過する)を引いた数は、Kの数以下であり、それで十分です。

これは、各ポイントから重心までの距離がわかった後、disを小さいものから大きいものに並べ替え(並べ替えはKより小さいポイントのペアを計算することです)、それらをペアで追加し、重心を除くすべてのポイントを追加することを意味します。次のようなすべての組み合わせをクリックします。

dis [2] + dis [5]は2——1——3に対応します。

dis [3] + dis [5]は4——2——1——3に対応します。

ただし、dis [3] + dis [4]が4——2——1——2——5に対応するなど、良くない状況があります。

ポイント4と5は、重心ポイント1を通過する必要はありませんでした。これをどのように排除できますか?まず、このポイントペアの特性を調べます。これらのポイントのペアがすべてサブツリーにあることは簡単にわかります。4と5の両方が2を介して重心に接続されています。言い換えれば、ポイントペアが重心の子ノードを通過する場合、それらは違法です。この判断条件で、それを排除することができます。

消去方法を理解した後、作業機能について説明します。その入力パラメータは、ノードの開始および開始から親ノードまでの距離(重み)です。

開始点までのすべての組み合わせを計算するために使用されます(合法および違法がカウントされます)。最初のdfs関数には、重心があります。作業関数は重心によって1回呼び出され、入力開始点は重心であり、重みは0であり、重心を通過するすべてのポイントペアが計算されます。dfsに戻り、重心のすべての子ノードをトラバースし、個別に作業を呼び出します。着信開始は子ノードであり、重みは子ノードから重心までの重みであり、現在の子ノードを通過するすべてのポイントペアを計算します。子ノードのポイントペアの後、計算された距離はまだ重心までです。重心を通過する点ペアであろうと子ノードを通過する点ペアであろうと、kと比較する必要があるため、計算する距離は重心までである必要があります。

// 传入的start要么是重心,weight=0
// 要么是重心的孩子结点,weight是重心与孩子结点的距离(权重)
int work(int start,int weight) {
    t=0;
    // 如果start是重心,算出 以重心为根的树中的结点 到start的距离,然后两两组合,选出加起来小于k的点对
    // 如果start是重心的孩子结点,算出 以该孩子结点为根 的子树中的结点 到重心的距离,然后两两组合,选出加起来小于k的点对
    // eg:重心是S的孩子结点是a,那么算出的点对数是 以a为根的树 的所有结点 两两组合,但是距离算的是 所有结点 到S的距离,因为仍然要判断小于k,和经过重心的点对要一致
    // getDistance需要传入weight就是为了 重心的孩子结点的 孩子结点的 dis是到重心的距离
    getDistance(start, 0, weight);
    // 得到dis数组后,对其进行从小到大排序
    // 注意,这里的t已经不是0了,它是全局变量,在getDistance里面遍历了start出发的所有结点
    sort(dis+1,dis+1+t);
    // pair_num表示经过重心的点对数量
    int pair_num=0;
    int i=1,j=t;
    // 这个while循环就是把两个dis相加的和小于等于K的点对数量计算出来
    while (i<j){
        while (i<j && dis[i]+dis[j]>K) 
            j--;
        pair_num+=j-i;
        i++;
    }
    return pair_num;
}

4.重心(再帰)の子ノードに対して操作1、2、および3を繰り返します。

dfs関数が初めてノードを自由に通過して、上の図のポイント1のように、ツリー全体の重心を見つけることがよくあります。ここでは、ポイント1を使用して作業関数を呼び出し、合法および違法を含む、ポイント1までのすべてのポイント対数を検索します。次に、forループのポイント1の子ノード(上の図のポイント2と3)をトラバースします。ポイント2とポイント3を使用して、workを再度呼び出し、ポイント2と3をそれぞれ通過するすべてのポイントペアを見つけます。 、次にansを引くと、有効なポイントペアが得られます。

4と5はk未満のポイントペアだと思うかもしれませんが、それらを差し引くと消えますか?したがって、減算した後、ポイント2とポイント3はそれぞれdfsに渡されて再帰され、ポイント4とポイント5のペアがカウントされます。ansはグローバル変数であり、最初のdfsの後も累積および減算を続けます。

// 起始点是start,递归调用会dfs所有的结点
void dfs(int start){
    // 以start为起点找到重心
    // 注意,虽然一开始我们说了从树的任何一个点开始遍历都能找出一个确定的重心,
    // 但是这里从start开始,如果它有父结点,就不要再遍历的,只找以它为根的树的重心
    getGravity(start,0);
    // 用这个重心算出所有跨过该重心的路径数
    ans += work(gravity,0);
    // 标记这个重心已访问
    visited[gravity]=1;
    // 从重心开始访问子结点
    for(int i=first[start]; i; i=edge[i].next){
        int end=edge[i].end;
        if (visited[end]){
            continue;
        }
        // 减去以子结点为根的树的所有跨过子结点的路径数
        ans -= work(end, edge[i].weight);
        // 从子结点开始继续递归
        dfs(end);
    }
    return;
}

次に、main関数を配置します。

int main()
{
    // 输入结点个数
    scanf("%d",&n);
    // 输入每个结点的信息
    for(int i=1;i<n;i++)
    {
        int start, end, weight;
        scanf("%d %d %d",&start,&end,&weight);
        addNodeAndEdge(start,end,weight);
        addNodeAndEdge(end,start,weight);
    }
    dfs(1);
    printf("%d\n", ans);
    return 0;
}

最後に、テストされていない全体的なコードは、主に時間がないためですが、私はコードを完全に理解しており、テストには十分です。

#include <stdio.h>
#define MAX 10000;

// 全局变量
int n; // 所有结点数
int size[n],max_child[n],min=MAX;// 以n为根的树的结点数,以n为根的树的最大子树的结点数
int first[n+1],edge[(n-1)*2]// 顶点表,边表
int visited[n+1];// 标记已访问过的点
int dis[];// 每个点到重心的距离
int gravity=0;// 重心
int num=0,t=0;// 结点编号
int ans=0;// 最终结果:小于等于K的点对数量

void addNodeAndEdge(int start, int end, int weight)
{
    num++;// 编号
    edge[num].start=start;// 起点
    edge[num].end=end;// 终点
    edge[num].weight=weight;// 权重
    edge[num].next=first[start];// 下一条相同起点的边
    first[start]=num;// 加入顶点
}

// 传入重心,以及重心和它的父结点之间的权重
int work(int start,int weight) {
    t=0;
    // 算出start到各个结点的距离
    getDistance(start, 0, weight);
    // 得到dis数组后,对其进行从小到大排序
    // 注意,这里的t已经不是0了,它是全局变量,在getDistance里面遍历了start出发的所有结点
    sort(dis+1,dis+1+t);
    // pair_num表示点对数量
    int pair_num=0;
    int i=1,j=t;
    // 这个while循环就是把两个dis相加的和小于等于K的点对数量计算出来
    while (i<j){
        while (i<j && dis[i]+dis[j]>K) j--;
        pair_num+=j-i;
        i++;
    }
    return pair_num;
}

void getDistance(int start, int parent, int weight)//fa表示x的父亲,z表示x到目标点的距离
{
    // dis数组保存了每个点到重心的距离(权重),为什么用t来做下标而不是点的编号呢
    // 因为后面的做法只用数点对的个数,不在乎是谁到谁
    dis[++t]=weight;
    for(int i=first[start]; i; i=edge[i].next)
    {
        int end=edge[i].end;
        // 如果这个终点已经被遍历或者这个终点是父结点,就跳过
        // 这点很重要,因为如果传入的start不是根结点而是根结点的子树的时候
        // 它就不会把根结点再遍历一次
        if(visited[end] || end==parent){
            continue;
        }
        getDistance(end, start, weight+edge[i].weight);
    }
}
// 传入参数
// start 代表当前结点
// parent 代表当前结点的父结点,这里是为了防止遍历start的子结点的时候把父结点也遍历进去了
void getGravity(int start, int parent)
{
    // size[start]代表当前结点的个数,初始为1是算上本身
    size[start]=1;
    // max_child[start]代表当前结点的最大子树结点数
    max_child[start]=0;
    // 遍历以start结点为起点的所有边(除去连接父结点的边)
    for(int i=first[start]; i; i=edge[i].next)
    {
        // end为当前边的终点(也是start点的子结点)
        int end=edge[i].end;
        // 如果这个终点已经被遍历或者这个终点是父结点,就跳过
        if(visited[end] || end==parent){
            continue;
        }
        // 继续遍历终点的子结点,这里其实就是遍历start的一棵子树的所有结点数
        getGravity(end, start);
        // 遍历完这棵子树的所有结点后,把子树的结点数加起来
        size[start]+=size[end];
        // 如果这颗子树的结点数大于max_child,就更新它
        if(size[end] > max_child[start]){
            max_child[start]=size[end];
        }
    }
    // 上面的循环是用来遍历start的每一棵子树并比较出最大子树结点
    // 接下来就是算“红色部分”,也就是n-size[i]部分的结点数,并比较出最终的最大子树结点
    if(n-size[start] > max_child[start]){
        max_child[start]=n-size[start];
    }
    // 从max_child中比较出最小的,以找出重心
    if(min < max_child[start]){
        min=max_child[x];
        gravity=start;
    }
}

// 递归地求每个树的经过重心的点对数量
// 起始点是start
void dfs(int start){
    // 以start为起点找到重心
    // 注意,虽然一开始我们说了从树的任何一个点开始遍历都能找出一个确定的重心,
    // 但是这里的意思是,从start开始,它的父结点就不要再遍历的,只找它和它的子树的重心
    getGravity(start,0);
    // 用这个重心算出所有跨过该重心的路径数
    ans += work(gravity,0);
    // 标记这个重心已访问
    visited[gravity]=1;
    // 从重心开始访问子结点
    for(int i=first[start]; i; i=edge[i].next){
        int end=edge[i].end;
        if (visited[end]){
            continue;
        }
        // 减去以子结点为根的树的所有跨过子结点的路径数
        ans -= work(end, edge[i].weight);
        // 以子结点为根的树继续求重心、所有跨过路径数
        dfs(end);
    }
    return;
}

int main()
{
    // 输入结点个数
    scanf("%d",&n);
    // 输入每个结点的信息
    for(int i=1;i<n;i++)
    {
        int start, end, weight;
        scanf("%d %d %d",&start,&end,&weight);
        addNodeAndEdge(start,end,weight);
        addNodeAndEdge(end,start,weight);
    }
    dfs(1);
    printf("%d\n", ans);
    return 0;
}

時間の複雑さは一時的に計算されません。

おすすめ

転載: blog.csdn.net/qq_33514421/article/details/112379820