浅谈批量连边
louiesxl
摘要
本文讨论了图论中批量将满足某些条件的两个点集互相连边的具体方法。
引言
图论中有时会出现这样一类题:
有 n n n个点, m m m次连边,每次从 s l i sl_i sli- s r i sr_i sri号点各连一条1权边到 t l i tl_i tli - t r i tr_i tri。最后求某种操作(比如最短路)。
通常暴力连边不会被通过,我们需要寻找更优秀的方案。
一个小优化
有n个点
a
1
a_1
a1-
a
n
a_n
an和另外n个点
b
1
b_1
b1-
b
n
b_n
bn。
要求给这些点两两连无向边,权值为1,
n
≤
1000000
n \leq 1000000
n≤1000000。
本例中接下来未经声明方向的边皆为无向边,未经声明权值的边皆为1权边。
有一种连边方案是这样的,两两暴力连边,以3个点为例:
总共连了 n 2 n^2 n2条边,效率不够高。
此种方法下称暴力连边
接下来我们换一种方式,创建两个新的节点 A A A, B B B,所有 a i a_i ai节点与 A A A连0权边,所有 B B B与 b i b_i bi连0权边,然后 A A A与 B B B连1权边。
如下图,任然是3个点,但采用上述方法:
总共连了
2
×
n
+
2
2 \times n +2
2×n+2个点,相比原始方法有很大提升,有向图大致相同。
此种方法下称直接连边
具体给出代码(C++)
//原始方法
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
add(a[i],b[j],1);
//改进方法
add(n+1,n+2,1);
for(int i=1;i<=n;i++)
add(a[i],n+1,0),add(n+2,b[i],0);
//p.s. 有向图也可以使用上述代码,但注意 add 函数的实现。
分块帮助连边
给定 n n n个点,有 m m m次连有向边计划,每次可以从 s l i sl_i sli- s r i sr_i sri号节点连往 t l i tl_i tli- t r i tr_i tri号节点,最后求一次从 1 1 1号节点到 n n n号节点的最短路,保证联通, n ≤ 500000 n \leq 500000 n≤500000, m ≤ 100000 m \leq 100000 m≤100000, 保证数据合理。
此时如果我们沿用上例批量连边的方案很明显是不行的,要连 O ( n × m ) O(n \times m) O(n×m)条边,必定 ME。
此时我们看向例1的方案,可以认为A点代表了所有 a 1 a_1 a1 - a n a_n an点,B点代表了所有 b 1 b_1 b1 - b n b_n bn 点,也就是一部分点代表了其他点。
在这里,我们可以考虑分块,对于 n n n个点,将他们分为 n \sqrt{n} n块(细节不做讨论),每次将整块和单个结合,用例1的方式连边。
如下图,是一个 n = 9 n = 9 n=9,从 1 − 5 1-5 1−5号连到 7 − 8 7-8 7−8号的例子,(假设这是第一次连边)。
分块新建
O
(
2
×
n
)
O(2 \times \sqrt n)
O(2×n)个点,每次直接连边新建
2
2
2个点
如果排除预处理的分块边,每次直接连边新建
O
(
2
×
n
)
O(2 \times \sqrt n)
O(2×n)条边。
**然而,即使是这样,仍然不够优秀。 **
改分块为线段树
考虑线段树,有了分块的解释,我们直接看图,下图是4个节点的 ,连接 1 − 3 1-3 1−3号和 4 4 4号,仍然假设是第1次连边。
线段树增加
O
(
n
)
O(n)
O(n)个节点,不考虑线段树,每次新增
O
(
2
×
log
n
)
O(2 \times \log n)
O(2×logn) 条边,效率可以通过,最后跑一遍01-bfs
或者dijkstra算法堆优化
即可!
具体来说可以参考如下代码。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
const int N=500009,M=100009;
int n,m,s,ns;
const int V=N*3+M*4,E=N*4+M*44;//新图点和边计算,代码后文章说明
int ls[V],rs[V];
int hd[V],to[E],w[E],nxt[E],ed;//链式前向星
inline void add(int u,int v,int c=0)//基础建边函数
{
ed++,nxt[ed]=hd[u],to[ed]=v,w[ed]=c,hd[u]=ed;
}
//以下为线段树,注意细节
int build(int l=1,int r=n)
{
if(l==r)
return l;
int mid=l+(r-l)/2,id=++ns;
ls[id]=build(l,mid),ls[id+n]=ls[id]+(ls[id]<=n?0:n);
rs[id]=build(mid+1,r),rs[id+n]=rs[id]+(rs[id]<=n?0:n);
add(id,ls[id]),add(id,rs[id]);
add(ls[id+n],id+n),add(rs[id+n],id+n);
return id;
}
void upd(int L,int R,bool T,int l=1,int r=n,int id=n+1)
{
if(L<=l&&r<=R)
{
clog.flush();
if(T)
add(ns,id);
else
add(id+(id<=n?0:n),ns);
return;
}
int mid=l+(r-l)/2;
if(L<=mid)
upd(L,R,T,l,mid,ls[id]);
if(R>mid)
upd(L,R,T,mid+1,r,rs[id]);
}
void add_o(int sl,int sr,int tl,int tr)//操作线段树的建边函数
{
ns++,add(ns,ns+1,1),upd(sl,sr,0);
ns++,upd(tl,tr,1);
}
//接下来为01-bfs,也要注意细节
int dst[V];
bitset <V> vst;
deque <int> q,ans;
void bfs()
{
fill(dst,dst+V,1e9);
q.push_front(s),ans.push_back(0),dst[s]=0;
while(!q.empty())
{
int u=q.front(),c=ans.front();
q.pop_front(),ans.pop_front();
if(vst[u])
continue;
vst[u]=1,dst[u]=min(dst[u],c);
for(int k=hd[u]; k; k=nxt[k])
{
int v=to[k],nc=w[k];
if(vst[v])
continue;
if(nc==0)
q.push_front(v),ans.push_front(c+nc);
}
for(int k=hd[u]; k; k=nxt[k])
{
int v=to[k],nc=w[k];
if(vst[v])
continue;
if(nc==1)
q.push_back(v),ans.push_back(c+nc);
}
}
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>s,ns=n;
build();
ns=ns+n;
for(int i=1,sl,sr,tl,tr; i<=m; i++)//建边
cin>>sl>>sr>>tl>>tr,add_o(sl,sr,tl,tr),add_o(tl,tr,sl,sr);
bfs();//01-bfs
for(int i=1; i<=n; i++)
cout<<dst[i]<<endl;
return 0;
}
接下来我们计算新图点数和边数。
点:
- 原图: n n n。
- 两个线段树: 2 × n 2\times n 2×n(叶子节点是原图点,两颗树共享)。
- 建边产生的点: 2 × m 2 \times m 2×m。
共 4 × n + 3 × m = 2000000 + 300000 = 2300000 4 \times n + 3\times m = 2000000+300000 = 2300000 4×n+3×m=2000000+300000=2300000个点。(保险起见多开了一点)。
边:
- 线段树: 2 × ( 2 × n ) 2 \times (2 \times n) 2×(2×n)。(根据上面点数算出来的,每个点最多 2 2 2条边连儿子)
- 建边: m × ( 2 × log n + 1 ) m \times (2 \times \log n +1) m×(2×logn+1)。
共建立的边: 4 × n + 44 × m = 2000000 + 4400000 = 6400000 4 \times n+ 44 \times m = 2000000 + 4400000=6400000 4×n+44×m=2000000+4400000=6400000(我们把 log n \log n logn当成20计算,后面 m m m的部分保险起见多开了一点)。
当点少边多时
我们是否可以把 n n n和 m m m更改一下呢? n ≤ 100000 n \leq 100000 n≤100000, m ≤ 500000 m \leq 500000 m≤500000。
计算点与边:点( 400000 + 1500000 = 1900000 400000+1500000=1900000 400000+1500000=1900000 ,边( 400000 + 22000000 400000+22000000 400000+22000000),边数在边缘地区,有点危险。
不考虑各种常数型改动,我们最好找到一种新的代表方式,考虑ST表。
因此节点数量变成了 n × log n n \times \log n n×logn,但每次新建立的边变成了4条(ST表性质, 2 2 2个节点可以拼凑出任意区间,这里一起采用第一部分中的暴力连边,每次 4 4 4条,由于直接连边的 5 5 5条)。
如下图, n = 4 n =4 n=4,从 1 − 3 1-3 1−3号连接到 4 4 4号,假设是第一次连接。
简单思考后就可以发现点数为 n × ( 2 × log n + 1 ) n \times (2 \times \log n +1) n×(2×logn+1),边数为 4 × n × log n + 2 × m 4 \times n \times \log n + 2 \times m 4×n×logn+2×m,大致可以接受。
非连续情况
此前我们的讨论基于一次批量为连续区块连接,如果不是连续的呢?可以认为,一定有某些规律。
例如此方案:给定 l x lx lx, l y ly ly, r x rx rx, r y ry ry,连接所有 l l l满足 l = l y + k × l x ( k ∈ N ) l=ly+k \times lx (k \in N) l=ly+k×lx(k∈N)和 r r r满足 r = r y + l × r x ( l ∈ N ) r=ry+l \times rx (l \in N) r=ry+l×rx(l∈N)。
这里考虑平衡规划,在此不做展开,大致思想如下:
- 如果 y y y超过 n \sqrt n n,则节点不超过 n \sqrt n n个,直接连边即可
- 如果 y y y不超过 n \sqrt n n,则只需预处理出 n × n n \times \sqrt n n×n个节点并连边,每次从表中寻找并暴力连边即可。
拓展情况
本文一直采用无权连边作例子,实际上有权也完全可以这样做:
比如线段树的那个例子(4个节点 ,连接
1
−
3
1-3
1−3号和
4
4
4号,假设是第1次连边),图无较大改动,只改动权值,注意最后就不能用01-bfs
了:
本文一直采用有向图做例子,实际上无向图完全可以类比,甚至简单了许多。
我们任然用上面的线段树做例子:
可以发现可以少建立一棵树。
结语
近来批量连边未在比赛中出现,实际上个人认为该问题并非与图论相关,其与数据结构关联性更强,我们涉及了连续连边时的分块,线段树与ST表思想建图,也讨论了等差建边时的平衡规划思想,实际上这类问题可以是任何一种奇怪的连边方式,我们只需要遵循少数点代表其他点的思想即可。
鸣谢
- 文档编辑: Stack Edit, Joplin
- 图论绘制: Windows7 附件中的画图软件
- 图片保存: ImgURL