图论-Dijkstra+线段树-bzoj3073-Journeys

权值线段树优化建图

问题描述

Description

Seter建造了一个很大的星球,他准备建造N个国家和无数双向道路。N个国家很快建造好了,用1…N编号,但是他发现道路实在太多了,他要一条条建简直是不可能的!于是他以如下方式建造道路:(a,b),(c,d)表示,对于任意两个国家x,y如果a<=x<=b,c<=y<=d,那么在xy之间建造一条道路。Seter保证一条道路不会修建两次,也保证不会有一个国家与自己之间有道路。
Seter好不容易建好了所有道路,他现在在位于P号的首都。Seter想知道P号国家到任意一个国家最少需要经过几条道路。当然,Seter保证P号国家能到任意一个国家。
注意:可能有重边

Input

第一行三个数N,M,P。N<=500000,M<=100000
后M行,每行4个数A,B,C,D。1<=A<=B<=N,1<=C<=D<=N。

Output

N行,第i行表示P号国家到第i个国家最少需要经过几条路。显然第P行应该是0。

Sample Input

5 3 4 
1 2 4 5 
5 5 4 4 
1 1 3 3

Sample Output

1 
1 
2 
0 
1

问题分析

  1. 问题其实求得就是个单源最短路径,所以用Dijkstra算法最好,用优先队列优化的Dijkstra算法的时间复杂度为O( (N + M) logN )。但是题目的N = 1e5,区间建边又会生成大量的边,甚至重边,即使去重,边的数量也很有可能达到N^2,这显然是不可接受的。
  2. 基于上述考虑,直接建图是不可取的。区间建边的方式让我们想到线段树一个节点表示一个区间的性质,所以我们可以把线段树上的节点当成图中的节点。
  3. 具体的线段树建图过程如下:
    1. 建立一颗线段树,叫做“出树”,代表离开一个节点的信息,出树的所有儿子建边指向父亲,边权为零。

      • 这可以理解为,从1出去,也是从[1,2]中出去,也是从[1,4]中出去,同时没有额外的花费。
    2. 建立另一颗线段树,叫做“入树”,代表进入一个节点的信息,入树的所有父亲建边指向儿子,边权为零。

      • 这可以理解为,进入[1, 4], 也就进入了[1, 2], 也就进入了1,同时没有花费。
    3. 两颗线段树叶子结点之间建边由入树指向出树。入树的底层1指向出树的底层1,入树的底层2指向出树的底层2。

      • 这可以理解为,进入这个节点后就可以离开这个节点,边权依旧为零。这样才能实现利用这个点的跳转。
    4. 区间加边:找到出树上[a, b]对应的区间节点,把它们连到一个新的虚拟节点1上面,边权为0;然后将虚拟节点1连到虚拟节点2上去,边权为1,;然后将虚拟节点2连接到入树上[c, d]对应的区间节点上,边权为0。这样,就实现了仅利用O(logN * logN + 2)条边完成O(N^2)条边连接的效果,使得边数大大减少。注意,由于原图是无向图,还得把相反的区间再以这个的办法再连一次
      image

      注:这图与我的描述不完全相符,但也能说明很大一部分问题,一切以描述为准

  4. 为实现以上想法,需要注意一下几点:
    • 所有点都得重新标定唯一编号。两颗线段树的编号不再是[1…n],所以根节点已不再是1, 需要分别记录两个树根ra、rb;这也导致不能使用p>>2和p>>2|1来指示左右孩子,所以必须设立数组lc[]、rc[]记录下每个点的左右孩子的vid
    • 记录下图中原始节点对应于出树的vid,即出树中叶子结点的vid,因为我们关心这些点到源点的距离。
    • 关于入树和出树的很多操作是类似的,可以重用一套函数,以一个flag来做差异化处理。
  5. 为了确定数组的规模,所以我们必须要计算空间复杂度,具体涉及到点数和边数两方面:
    1. 点数 N = 8n + 4m = 440,0000
      • 两棵线段树: 4n * 2
      • m次区间建边,双向边翻倍, 每次添加2个点: m *2 * 2
    2. 边数 M = 9n + 10m + 4mlogn = 1350,0000
      • 两棵线段树构成树的边: 4n * 2
      • 两棵线段树叶子节点连边: n
      • m次区间连边,双向边翻倍, 每次连 logn * 2 + 1条边: m * 2 * (2log(4n) + 1)

AC代码

只是用样例测试了下,没有在OJ提交过(找不到原题,似乎下架了?)

# include <cstdio>
# include <algorithm>
# include <cstring>
# include <cstdlib>
# include <queue>

using namespace std;
const bool DBG = true;
const int N = 4400000 + 10;//4n * 2 + 2m * 2 = 8n + 4m = 400 + 40 = 440
const int M = 13500000 + 10;//4n * 2 + n + 2m * (logn * logn + 1) = 9n + 2m((logn) * 2 + 1) = 450 + 820 = 1350
const int INF = 0x3f3f3f3f;

struct Vex
{
    int v,d;
    Vex(){}
    Vex(int _v,int _d):v(_v),d(_d){}
    friend bool operator<(const Vex & a, const Vex & b){return a.d>b.d;}
};

int n, m, s;
int eid , vid, id[N];//边和点的唯一编号,id[i]指示原始i节点的编号
int first[N], nxt[M], to[M], val[M];//链式前向星
int ra, rb, lc[N], rc[N];//ra、rb为入树、出树的根的编号,lc[i]、rc[i]指示i的左、右孩子
int dis[N];//dis[i]表示i距离与源点s的距离

//建立新边
//u -w-> v
inline void add(int u, int v, int w = 0)
{
    to[++eid] = v, val[eid] = w;
    nxt[eid] = first[u], first[u] = eid;;
}

//建立线段树,并记录每个点的编号vid
void build(int & p, int l, int r, bool out)
{
    if(!p) p = ++vid;
    if(l == r){if(out) id[l] = p; return;}
    int mid = l + ((r - l) >> 1);
    build(lc[p], l, mid, out);
    build(rc[p], mid + 1, r, out);
    if(out) add(lc[p], p), add(rc[p], p);
    else add(p, lc[p]), add(p, rc[p]);
}

//区间更新,将树p的点[st, ed]与点c连接
//(out)[st, ed] --> c
//(in)c -->[st, ed]
void update(int p, int l, int r, int st, int ed, int c, int out)
{
    if(st <= l && r <= ed)
    {
        if(out) add(p, c);
        else add(c, p);
        return;
    }
    int mid = l + ((r - l) >> 1);
    if(st <= mid) update(lc[p], l, mid, st, ed, c, out);
    if(ed > mid) update(rc[p], mid + 1, r, st, ed, c, out);
}

//连接ta、tb两颗线段树,但仅连接叶子结点
//(in)pa --> (out)pb
void link_ab(int pa, int pb, int l, int r)
{
    if(l == r){add(pa, pb); return;}
    int mid = l + ((r - l) >>1);
    link_ab(lc[pa], lc[pb], l, mid);
    link_ab(rc[pa], rc[pb], mid + 1, r);
}

//区间连边,从出树到入树
//(out)[a, b] --0--> vid --1--> vid + 1 --0--> (in)[c, d]
void link(int a, int b, int c, int d)
{
    update(rb, 1, n, a, b, ++vid, true);
    add(vid, vid + 1, 1);
    update(ra, 1, n, c, d, ++vid, false);
}

//单源最短路径-O((N+M)logN)
void dijkstra()
{
    priority_queue<Vex> q;
    memset(dis, INF, sizeof(dis));
    dis[id[s]] = 0;
    q.push(Vex(id[s], 0));
    while(!q.empty())
    {
        Vex cur = q.top(); q.pop();
        int u = cur.v, d = cur.d;
        if(d != dis[u])
            continue;
        for(int p = first[u]; p; p = nxt[p])
        {
            int v = to[p], w = val[p];
            if(dis[v] > dis[u] + w)
            {
                dis[v] = dis[u] + w;
                q.push(Vex(v, dis[v]));
            }
        }
    }
}

int main()
{
    scanf("%d%d%d", &n, &m, &s);
    build(ra, 1, n, false), build(rb, 1, n, true);
    link_ab(ra, rb, 1, n);
    for(int i =0; i < m; i++)
    {
        int a, b, c, d;
        scanf("%d %d %d %d", &a, &b, &c, &d);
        link(a, b, c, d), link(c, d, a, b);
    }
    dijkstra();
    for (int i = 1; i <= n; i++)
        printf("%d\n", dis[id[i]]);
    
    if(DBG)
    {
        for(int i = 1; i <= n; i++)
            printf("id[%d] = %d, dis[id[%d]] = %d\n", i, id[i], i, dis[id[i]]);
        printf("\n");
        printf("vid = %d eid = %d\n\n", vid, eid);
        printf("ra = %2d rb = %2d\n\n", ra, rb);
        for (int i = 1; i <= vid; i++)
            printf("vid = %2d dis = %10d lc = %2d rc = %2d\n", i, dis[i], lc[i], rc[i]);
        printf("\n");
        for(int i = 1 ; i <= vid ; i++)
        {
            printf("%d: ", i);
            for(int p = first[i]; p; p = nxt[p])
            {
                printf("[%2d] to = %2d val = %2d / ", p, to[p], val[p]);
            }
            printf("\n");
        }
    }
    system("pause");
    return 0;
}

参考博客

  1. https://www.cnblogs.com/OI-zzyy/p/11179327.html (分析给力,图示清晰)
  2. https://blog.csdn.net/lvzelong2014/article/details/79153621 (代码更符合我的习惯)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值