题解 [HDU5669]Road

题意简述

一张n个点的图,连边方式为区间[a, b]中每一个点向区间[c, d]中每一个点连带权无向边,求可使k条边权值变为0的最短路(其中起点为1,终点为n)
n \(\leq\) 50000, 连边次数m \(\leq\) 10000, K \(\leq\) 10, 边权w \(\leq\) 1000

分析

如果暴力连边的话,即使m = 1也需要连 1e9 次,根本无法承受
所以这题考虑对连边区间进行处理,即线段树优化建图,这也是最短路和网络流建图的一个常见套路
我们按照点的顺序构造两棵 单向边 的线段树,一棵的连边从 父节点子节点 ,另一棵则相反,从 子节点父节点

Q : 为啥要这样建线段树呢?我只搞一棵线段树不是更方便吗?

A : 考虑线段树父节点和子节点之间连边的意义:

  1. 大区间包含小区间 => 我都能走到这个区间,那我肯定能走到他的子区间
  2. 小区间属于大区间 => 如果一个包含我的区间能走到别的区间,那我肯定也能走到那个区间

不难发现,上述两种情形所需要的连边是不一样的,所以肯定是要两棵线段树的啦
放一下代码 [以自顶向下连边的线段树 (代码中为tree1) 为例]

void build(int l, int r, int x)
{
    max_siz = max(max_siz, x);
    if(l == r){
        tree1[l] = x, rtree1[x] = l;
        return ;
    }
    addedge(x, x << 1, 0); //边权为0应该很显然吧...
    addedge(x, x << 1 | 1, 0);
    int mid = (l + r) >> 1;
    build(l, mid, x << 1);
    build(mid + 1, r, x << 1 | 1);
}

上面的 max_siz 就是计数器,存储一棵线段树需要多少内存(毕竟还有第二棵,而且还要跑最短路)
tree1[i] 表示点 i 对应的叶子节点在线段树中的编号,其实他的唯一用处就是用来求 tree2[i] (相信你已经知道了,tree2中节点对应的编号就是tree1中的加上 max_siz )
rtree1 就是 tree 的反函数(这不废话)用来表示线段树中编号为i的点是哪个的叶子节点(当rtree1[i] = 0i 对应的点就不是叶子节点)
同样的,rtree2 根本就没有什么用,我甚至懒得定义他

Q :为啥 tree2rtree1 是有用的,而 tree1rtree2 并没有什么用?

A :本题规定起点为1,那你想想,在最后建成的图中,起点会是哪一个点?

  • 首先,起点肯定得是一个表示1的叶子节点,因为只有那样才能保证那个点只表示1而不包含其他的点
  • 其次,他会是哪一个叶子节点呢? 联系上一个问题,这时的情景是 :我是1,我需要找到一个包含我的区间,并通过他走向远方
  • 显然,这对应自底向上连边的tree2
  • 所以,tree2是有用的而tree1就只能成为工具人
  • 同理,终点自然需要用tree1中的点表示,故rtree1是有用的而rtree2甚至没必要出现

顺便说一下,有了tree1tree2的建立就没必要那么麻烦了,我们只需要将tree1对应节点加上max_siz再连边即可

好的,这个时候我们已经连好了线段树内部的边,现在来考虑两棵线段树之间的边该如何处理

  • 对于题目所给的区间连边,我们自然会考虑是tree2tree1连(废话,刚刚不是说了起点在tree2 ?)
  • 而如果只有这一种边的话,那想一下,tree1中的叶子节点岂不是出度为 0 ? 也就是说,我只要走到一个tree1的叶子节点我就走不动了
  • 所以,我们的tree1中的叶子节点应当向tree2中的叶子节点连边(边权自然是0啊)

这里顺便给出第一个问题的另外一种解释:两棵树的节点分别代表对应区间作为起点和终点所对应的点(好绕啊),那么既然是同一个点,终点向起点连边就十分自然了
所以这里本来要求tree1中所有的点都要向tree2中对应的点连边,但其实只要处理叶子节点就够了

简单放一下代码

int cnt1, cnt2, p1[35], p2[35];
void find(int L, int R, int l, int r, int x, int &cnt, int *p)
{
    if(L <= l && R >= r){
        p[++cnt] = x;
        return ;
    }
    int mid = (l + r) >> 1;
    if(L <= mid) find(L, R, l, mid, x << 1, cnt, p);
    if(R > mid) find(L, R, mid + 1, r, x << 1 | 1, cnt, p);
}
 
void add(int w)
{
    for(int i = 1; i <= cnt1; i++)
    for(int j = 1; j <= cnt2; j++)
    addedge(p1[i] + max_siz, p2[j], w),
    addedge(p2[j] + max_siz, p1[i], w);
}

int a, b, c, d, s;
for(int i = 1; i <= m; i++){
    cnt1 = cnt2 = 0;
    a = read(), b = read(), c = read(), d = read(), s = read();
    find(a, b, 1, n, 1, cnt1, p1);
    find(c, d, 1, n, 1, cnt2, p2);
    add(s);
}

简单来说就是将目标区间在线段树中对应区间的编号存储到两个数组中再一一连边

Q :那复杂度不是 O(\(log\)2 n) ? 万一炸了怎么办 ?
A :这题绝对炸不了,实在不放心也可以利用这 O(\(log\)2 n) 条边边权相等的性质,建立一个中转点u,将tree2中的点连向u,在把u连向tree1,这样能干掉一个\(log\)

Q :为啥没出现tree1叶子节点连向tree2
A :不急,在后面

好的,我们终于把连边处理完了,那么问题来了

Q :那消除k条边的费用呢?
A :分层图最短路(可惜空间太小,我写不下)

观察发现k很小,我们直接在dijstra(别问我为什么不用SPFA)的dis数组上加一维
代码如下

struct node{
    int d, num, o;//d 就是 dis,num 是节点编号,o 是我消除了多少条边的费用
    bool operator <(const node a)const{
        if(d != a.d)return d > a.d;
        else return o > a.o;//一样的以d为第一关键字排序,这里不过是加了o这个第二关键字而已
    }
};
priority_queue < node > q;

void calc(int d, int u, int c)//calc就是dijtra的拓展函数
{
    for(int i = head[u]; i; i = nxt[i]){
        int v = to[i], nd = w[i];
        //分类讨论:我消除或不消除这条边的花费
        if(dis[v][c] > d + nd){
            dis[v][c] = d + nd;
            node cur = {d + nd, v, c};
            q.push(cur);
        }
        if(c + 1 <= k && dis[v][c + 1] > d){
            dis[v][c + 1] = d;
            node cur = {d, v, c + 1};
            q.push(cur);
        }
    }
}
 
void dijstra()
{
    for(int i = 1; i <= (max_siz << 1); i++)
        for(int j = 0; j <= k; j++) dis[i][j] = inf;
    for(int i = 0; i <= k; i++) dis[tree2[1]][i] = 0;
     
    node cur = {0, tree2[1], 0};
    q.push(cur);
     
    while(!q.empty()){
        cur = q.top(); q.pop();
        int d = cur.d, u = cur.num, c = cur.o;
        if(dis[u][c] < d) continue;
        calc(d, u, c);
        if(rtree1[u]) calc(d, u + max_siz, c); // 这就是刚刚说的tree1叶子连向tree2叶子啦
    }
}

好的,接下来我们就成功的写完了这道题了

代码

不放了...

简单的总结

本题应该是一道非常套路的线段树优化建图 + 分层图最短路问题,当然,在看到区间连边时我们还应该考虑到建立中转点是否可行(不过这题O(nmk \(log\) n)肯定是不行的)。
顺便祝大家调题愉快!

上一篇:CPU模式与状态


下一篇:ARM 异常处理过程