权值线段树优化建图
问题描述
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
问题分析
- 问题其实求得就是个单源最短路径,所以用Dijkstra算法最好,用优先队列优化的Dijkstra算法的时间复杂度为O( (N + M) logN )。但是题目的N = 1e5,区间建边又会生成大量的边,甚至重边,即使去重,边的数量也很有可能达到N^2,这显然是不可接受的。
- 基于上述考虑,直接建图是不可取的。区间建边的方式让我们想到线段树一个节点表示一个区间的性质,所以我们可以把线段树上的节点当成图中的节点。
- 具体的线段树建图过程如下:
-
建立一颗线段树,叫做“出树”,代表离开一个节点的信息,出树的所有儿子建边指向父亲,边权为零。
- 这可以理解为,从1出去,也是从[1,2]中出去,也是从[1,4]中出去,同时没有额外的花费。
-
建立另一颗线段树,叫做“入树”,代表进入一个节点的信息,入树的所有父亲建边指向儿子,边权为零。
- 这可以理解为,进入[1, 4], 也就进入了[1, 2], 也就进入了1,同时没有花费。
-
在两颗线段树叶子结点之间建边,由入树指向出树。入树的底层1指向出树的底层1,入树的底层2指向出树的底层2。
- 这可以理解为,进入这个节点后就可以离开这个节点,边权依旧为零。这样才能实现利用这个点的跳转。
-
区间加边:找到出树上[a, b]对应的区间节点,把它们连到一个新的虚拟节点1上面,边权为0;然后将虚拟节点1连到虚拟节点2上去,边权为1,;然后将虚拟节点2连接到入树上[c, d]对应的区间节点上,边权为0。这样,就实现了仅利用O(logN * logN + 2)条边完成O(N^2)条边连接的效果,使得边数大大减少。注意,由于原图是无向图,还得把相反的区间再以这个的办法再连一次。
注:这图与我的描述不完全相符,但也能说明很大一部分问题,一切以描述为准
-
- 为实现以上想法,需要注意一下几点:
- 所有点都得重新标定唯一编号。两颗线段树的编号不再是[1…n],所以根节点已不再是1, 需要分别记录两个树根ra、rb;这也导致不能使用p>>2和p>>2|1来指示左右孩子,所以必须设立数组lc[]、rc[]记录下每个点的左右孩子的vid。
- 记录下图中原始节点对应于出树的vid,即出树中叶子结点的vid,因为我们关心这些点到源点的距离。
- 关于入树和出树的很多操作是类似的,可以重用一套函数,以一个flag来做差异化处理。
- 为了确定数组的规模,所以我们必须要计算空间复杂度,具体涉及到点数和边数两方面:
- 点数 N = 8n + 4m = 440,0000
- 两棵线段树: 4n * 2
- m次区间建边,双向边翻倍, 每次添加2个点: m *2 * 2
- 边数 M = 9n + 10m + 4mlogn = 1350,0000
- 两棵线段树构成树的边: 4n * 2
- 两棵线段树叶子节点连边: n
- m次区间连边,双向边翻倍, 每次连 logn * 2 + 1条边: m * 2 * (2log(4n) + 1)
- 点数 N = 8n + 4m = 440,0000
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;
}
参考博客
- https://www.cnblogs.com/OI-zzyy/p/11179327.html (分析给力,图示清晰)
- https://blog.csdn.net/lvzelong2014/article/details/79153621 (代码更符合我的习惯)