HDU 4126 Genghis Khan the Conqueror prim + 树形DP 好题

探讨了在一个无向图中生成最小生成树后,针对特定边权的变化如何高效更新最小生成树的问题。通过预处理及树形动态规划的方法,实现了对大量查询的快速响应。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载:http://www.cnblogs.com/ACMan/archive/2012/10/07/2713690.html

题意:

一个N个点的无向图,先生成一棵最小生成树,然后给你Q次询问,每次询问都是x,y,z的形式, 表示的意思是在原图中将x,y之间的边增大(一定是变大的)到z时,此时最小生成数的值是多少。最后求Q次询问最小生成树的平均值。 N<=3000 , Q<=10000

思路:

先求出该图的最小生成树,用prim(), O(n^2)。

对于每次询问, 都是将a,b之间的边增加到c, 会出现 两种情况:

1. 如果边权增加的那条边原先就不在最小生成树中,那么这时候的最小生成树的值不变

2. 如果在原最小生成树中,那么这时候将增加的边从原最小生成树中去掉,这时候生成树就被分成了两个各自联通的部分,可以证明的是,这时候的最小生成树一定是将这两部分联通起来的最小的那条边。

题转化:

首先我们先求出最小生成树,然后将在最小生成树中边去掉,对于每条最小生成树中的边,我们要求出它的替代边, 并且要求该替代边最小。

对于询问Q(10000) 分析一下Q里面的复杂度必须是O(n) 或 O(1)。

我们需要在外面预处理求出一些跟能推出答案但推出过程的复杂度是以上2个的其中1个。

 

 

 方法一:

假设两个各自连通的部分分别为树A,树B

1. 用dp[i][j]表示树A中的点i 到 树B(j点所在的树)的最近距离,这个过程可以在一边dfs就可以出来,对于每个 i 的dfs 复杂度是O(n) ,外加一个n的循环求出每个点,这里的总复杂度为 O(n^2)。

2. 通过求出来的dp[i][j] 再用一个dfs 求出  树B 到 树A的最近距离,(方法:枚举树A中的所有点 到 树B的最近距离,取其中的最小值。)显然, 这个求出来的值是我们要的最小替代边,把它保存到一个best[i][j]数组里面,(best[i][j]表示去掉边<i,j>后它的最小替代边的值)这里的总复杂度为 O(n^2)。

代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;

const int maxn = 3003;
const int maxm = maxn * maxn;
const int inf = 1000000000;

int n, m;
__int64 mst;
int map[maxn][maxn];
int dp[maxn][maxn], best[maxn][maxn];
int dis[maxn], pre[maxn];
bool vis[maxn];
vector<int> edge[maxn];

int minz(int a, int b)
{
    return a < b ? a : b;
}
void init()
{
    int i, j;
    for(i = 0; i < n; i++)
    {
        for(j = 0; j < n; j++)
            map[i][j] = dp[i][j] = inf;
        edge[i].clear();
        vis[i] = 0;
        pre[i] = -1;
        dis[i] = inf;
    }
}
void input()
{
    int x, y, z;
    while(m--)
    {
        scanf("%d%d%d", &x, &y, &z);
        map[x][y] = map[y][x] = z;
    }
}

void prim()
{
    int i, j, k;
    for(i = 1; i < n; i++)
    {
        dis[i] = map[0][i];
        pre[i] = 0;
    }
    dis[0] = inf;
    vis[0] = 1;
    pre[0] = -1;
    mst = 0;
    
    for(i = 0; i < n-1; i++)
    {
        k = 0;
        for(j = 1; j < n; j++)
            if(!vis[j] && dis[k] > dis[j])
                k = j;

        vis[k] = 1;
        mst += dis[k];
       //建最小生成树
        if(pre[k] != -1)
            edge[k].push_back(pre[k]),
            edge[pre[k]].push_back(k);

        for(j = 1; j < n; j++)
            if(!vis[j] && dis[j] > map[k][j] )
                dis[j] = map[k][j], pre[j] = k;
    }
}

int dfs1(int u, int fa, int rt) // 求 点rt 到 以u为根的数及其子树的最小距离
{
    int i;
    for(i = 0; i < edge[u].size(); i++)
    {
        int v = edge[u][i];
        if(v == fa) continue;
        dp[rt][u] = minz(dp[rt][u], dfs1(v, u, rt));
    }
    if(fa != rt) dp[rt][u] = minz(dp[rt][u], map[rt][u]);
    return dp[rt][u];
}

int dfs2(int u, int fa, int rt) // 求 以rt为根的数及其子树 到 以u为根的数及其子树的最小距离
{
    int i;
    int ans = dp[u][rt];
    for(i = 0; i < edge[u].size(); i++)
    {
        int v = edge[u][i];
        if(v == fa) continue;
        ans = minz(ans, dfs2(v, u, rt));
    }
    return ans;
}

void solve()
{
    int i,j;
    for(i = 0; i < n; i++)
        dfs1(i, -1, i);
    for(i = 0; i < n; i++)
        for(j = 0; j < edge[i].size(); j++)
        {
            int v = edge[i][j];
            best[i][v] = best[v][i] = dfs2(v, i, i);
        }
}
void query()
{
    int x, y, z;
    double sum = 0;
    scanf("%d", &m);
    for(int ii = 1; ii <= m; ii++)
    {
        scanf("%d%d%d", &x, &y, &z);
        if(pre[x] != y && pre[y] != x)
            sum += mst * 1.0;
        else
            sum += mst * 1.0 - map[x][y] + minz(best[x][y], z);
    }
    printf("%.4f\n", sum/m);
}
int main()
{
    while( ~scanf("%d%d", &n, &m) && n + m)
    {
        init();
        input();
        prim();
        solve();
        query();
    }
    return 0;
}

下面一个代码是跟着别人写的,思路差不多,但很搓,很难解释,它的做法是求出树的前序遍历,然后在数组里面实现树形DP的更新功能,以上的第一步它在询问Q外面做,第二步在询问Q里面做的,我保存其代码的原因是他树形DP的实现很犀利,然后prim用邻接表写的,我这个弱菜不会,很多地方值得学习。

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;

const int maxn = 3003;
const int maxm = maxn * maxn;
const int inf = 1000000000;

int n, m;
__int64 mst;
int map[maxn][maxn];
int dp[maxn][maxn], best[maxn][maxn];
int dis[maxn], pre[maxn];
bool vis[maxn];
vector<int> edge[maxn];

int minz(int a, int b)
{
    return a < b ? a : b;
}
void init()
{
    int i, j;
    for(i = 0; i < n; i++)
    {
        for(j = 0; j < n; j++)
            map[i][j] = dp[i][j] = inf;
        edge[i].clear();
        vis[i] = 0;
        pre[i] = -1;
        dis[i] = inf;
    }
}
void input()
{
    int x, y, z;
    while(m--)
    {
        scanf("%d%d%d", &x, &y, &z);
        map[x][y] = map[y][x] = z;
    }
}

void prim()
{
    int i, j, k;
    for(i = 1; i < n; i++)
    {
        dis[i] = map[0][i];
        pre[i] = 0;
    }
    dis[0] = inf;
    vis[0] = 1;
    pre[0] = -1;
    mst = 0;
    
    for(i = 0; i < n-1; i++)
    {
        k = 0;
        for(j = 1; j < n; j++)
            if(!vis[j] && dis[k] > dis[j])
                k = j;

        vis[k] = 1;
        mst += dis[k];
       //建最小生成树
        if(pre[k] != -1)
            edge[k].push_back(pre[k]),
            edge[pre[k]].push_back(k);

        for(j = 1; j < n; j++)
            if(!vis[j] && dis[j] > map[k][j] )
                dis[j] = map[k][j], pre[j] = k;
    }
}

int dfs1(int u, int fa, int rt) // 求 点rt 到 以u为根的数及其子树的最小距离
{
    int i;
    for(i = 0; i < edge[u].size(); i++)
    {
        int v = edge[u][i];
        if(v == fa) continue;
        dp[rt][u] = minz(dp[rt][u], dfs1(v, u, rt));
    }
    if(fa != rt) dp[rt][u] = minz(dp[rt][u], map[rt][u]);
    return dp[rt][u];
}

int dfs2(int u, int fa, int rt) // 求 以rt为根的数及其子树 到 以u为根的数及其子树的最小距离
{
    int i;
    int ans = dp[u][rt];
    for(i = 0; i < edge[u].size(); i++)
    {
        int v = edge[u][i];
        if(v == fa) continue;
        ans = minz(ans, dfs2(v, u, rt));
    }
    return ans;
}

void solve()
{
    int i,j;
    for(i = 0; i < n; i++)
        dfs1(i, -1, i);
    for(i = 0; i < n; i++)
        for(j = 0; j < edge[i].size(); j++)
        {
            int v = edge[i][j];
            best[i][v] = best[v][i] = dfs2(v, i, i);
        }
}
void query()
{
    int x, y, z;
    double sum = 0;
    scanf("%d", &m);
    for(int ii = 1; ii <= m; ii++)
    {
        scanf("%d%d%d", &x, &y, &z);
        if(pre[x] != y && pre[y] != x)
            sum += mst * 1.0;
        else
            sum += mst * 1.0 - map[x][y] + minz(best[x][y], z);
    }
    printf("%.4f\n", sum/m);
}
int main()
{
    while( ~scanf("%d%d", &n, &m) && n + m)
    {
        init();
        input();
        prim();
        solve();
        query();
    }
    return 0;
}

点击打开链接

方法二:

 假设两个各自连通的部分分别为树A,树B

用dp[i][j]表示树A(i点所在的树) 到 树B(j点所在的树)的最近距离。

dfs 的功能是 求树A中的点i 到 树B(j点所在的树)的最近距离,跟上面一种方法一样,对于每个 i 的dfs 复杂度是O(n) ,外加一个n的循环求出每个点,这里的总复杂度为 O(n^2)。 一边进行dfs,一边通过每一层的dfs返回值来更新dp[i][j]。  想法很犀利,代码更很犀利。

具体看代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;

const int maxn = 3003;
const int maxm = maxn * maxn;
const int inf = 1000000000;

int n, m;
__int64 mst;
int map[maxn][maxn];
int dp[maxn][maxn];
int dis[maxn], pre[maxn];
bool vis[maxn];
vector<int> edge[maxn];

int minz(int a, int b)
{
    return a < b ? a : b;
}
void init()
{
    int i, j;
    for(i = 0; i < n; i++)
    {
        for(j = 0; j < n; j++)
            map[i][j] = dp[i][j] = inf;
        edge[i].clear();
        vis[i] = 0;
        pre[i] = -1;
        dis[i] = inf;
    }
}
void input()
{
    int x, y, z;
    while(m--)
    {
        scanf("%d%d%d", &x, &y, &z);
        map[x][y] = map[y][x] = z;
    }
}

void prim()
{
    int i, j, k;
    for(i = 1; i < n; i++)
    {
        dis[i] = map[0][i];
        pre[i] = 0;
    }
    dis[0] = inf;
    vis[0] = 1;
    pre[0] = -1;
    mst = 0;
    
    for(i = 0; i < n-1; i++)
    {
        k = 0;
        for(j = 1; j < n; j++)
            if(!vis[j] && dis[k] > dis[j])
                k = j;

        vis[k] = 1;
        mst += dis[k];

        if(pre[k] != -1)
            edge[k].push_back(pre[k]),
            edge[pre[k]].push_back(k);

        for(j = 1; j < n; j++)
            if(!vis[j] && dis[j] > map[k][j] )
                dis[j] = map[k][j], pre[j] = k;
    }
}

int dfs(int pos, int u, int fa) //求pos 点 到 以u为根的树及其子树的最小距离
{
    int i, ans = inf;
    for(i = 0; i < edge[u].size(); i++)
    {
        int v = edge[u][i];
        if(v == fa) continue;
        int tmp = dfs(pos, v, u);
        ans = minz(ans, tmp);
        dp[u][v] = dp[v][u] = minz(dp[u][v], tmp); //通过dfs的返回值来更新dp[i][j],怎么更新自己理解吧,我也说不清楚
    }
    if(pos != fa) //保证这条边不是生成树的边, 不然不能更新
        ans = minz(ans, map[pos][u]);
    return ans;
}

void solve()
{
    int i;
    for(i = 0; i < n; i++)
        dfs(i, i, -1);
}

void query()
{
    int x, y, z;
    double sum = 0;
    scanf("%d", &m);
    for(int ii = 1; ii <= m; ii++)
    {
        scanf("%d%d%d", &x, &y, &z);
        if(pre[x] != y && pre[y] != x)
            sum += mst * 1.0;
        else
            sum += mst * 1.0 - map[x][y] + minz(dp[x][y], z);
    }
    printf("%.4f\n", sum/m);
}
int main()
{
    while( ~scanf("%d%d", &n, &m) && n + m)
    {
        init();
        input();
        prim();
        solve();
        query();
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值