20200721005922
给定一个 n n n 点 m m m 边的无向图 G = ( V , E ) G=(V,E) G=(V,E),每个节点 u u u 有点权值 A u A_u Au。
于是每个节点就有【邻居点权值的集合】 S u = { A v ∣ ( u , v ) ∈ E } S_u = \{A_v | (u,v)\in E\} Su={Av∣(u,v)∈E},即集合中是 u u u 的各邻居点 v v v 的权值。
最后定义节点 u u u 的 MEX 值为最小的不存在于 S u S_u Su 中的非负整数。
有 q q q 次操作,每次要么修改某一点 u u u 的权值,要么查询某一点 u u u 的 MEX 值。
样 例 数 T ≤ 10 ; n ≤ 1 0 5 ; m ≤ 1 0 5 ; q ≤ 1 0 5 ; 各 点 权 值 ≤ 1 0 9 ; 样例数 T\leq 10; n\leq 10^5; m\leq 10^5; q\leq 10^5; 各点权值\leq 10^9; 样例数T≤10;n≤105;m≤105;q≤105;各点权值≤109;
(注:本文中的节点数 n n n、边数 m m m 均不区分大小写,大小写混用了……)
首先我们可以对每个节点 u u u 维护 S u S_u Su 的权值线段树,总共 n n n 棵。即,我们把 S u S_u Su 看做 multiset (集合内元素允许重复存在),线段树第 i i i 个叶子( 0 ≤ i ≤ 1 0 9 0\leq i\leq 10^9 0≤i≤109)维护的是权值 i i i 出现了几次,线段树区间的含义是对应叶子所代表权值的出现次数的最小值。这样,
(上面算是权值线段树基础知识)
然而,我们发现,每个节点 u u u 的 MEX 值不可能超过其度数(邻居数) deg u \deg_u degu ,因为,每个小于其 MEX 值的非负整数一定是 u u u 的至少一个邻居的权值(由 MEX 值定义知),而 u u u 只有 deg u \deg_u degu 个邻居。因此,上述每棵权值线段树不需要 O ( 1 0 9 ) O(10^9) O(109) 个叶子,只需 ( deg u + 1 ) (\deg_u+1) (degu+1) 个,其中第 deg u \deg_u degu 个叶子改为维护大于等于 deg u \deg_u degu 的权值出现了几次,即可决定 MEX 值。需要注意的是,此题中操作不包括对连边的修改,因此每个节点的度数是不变的。由于 2 m = ∑ u ∈ V deg u 2m=\sum_{u\in V} \deg_u 2m=∑u∈Vdegu (无向图的性质),叶子总数为 ∑ u ∈ V ( deg u + 1 ) = 2 m + n ≤ 3 ∗ 1 0 5 \sum_{u\in V}(\deg_u +1)=2m+n\leq 3*10^5 ∑u∈V(degu+1)=2m+n≤3∗105,则线段树只需开 1.2 ∗ 1 0 6 1.2*10^6 1.2∗106 个节点。
赛上已经考虑到,即使这样做仍有可能超时:如果其中 n = 1 0 5 n=10^5 n=105,几乎全部节点都与某节点 u 0 u_0 u0 连边,那么单次修改此节点权值就需要在 O ( 1 0 5 ) O(10^5) O(105) 棵线段树上做单点修改;若 q = 1 0 5 q=10^5 q=105 次询问几乎都做这件操作,则单点修改总次数达到 O ( 1 0 10 ) O(10^{10}) O(1010)。然而却不知道怎样优化。
题解也应用了上一章第二段的优化,使权值线段树们的总节点数可控。
对于上一章第三段提出的特别极端而难以应对的可能输入数据,我们发现问题本质在于度数大的节点(以下简称大节点)在修改权值时邻居太多、改不完它们的树。题解提出,这些大节点的数量是有限的,因此,让别的点查询时直接访问这些大节点来取值的耗时更短。具体来说,由于 ∑ u ∈ V deg u = 2 m ≤ 2 ∗ 1 0 5 \sum_{u\in V} \deg_u=2m\leq 2*10^5 ∑u∈Vdegu=2m≤2∗105,因此度数大于 M A X _ D E G MAX\_DEG MAX_DEG 的节点不可能超过 2 m M A X _ D E G \frac{2m}{MAX\_DEG} MAX_DEG2m 个,它们可被视为大节点,其余视为小节点。我们令每个节点(无论大小)的权值线段树都只存储其邻居中的小节点的权值信息,
修改小节点的权值时,去修改其所有邻居(最多 M A X _ D E G MAX\_DEG MAX_DEG 个)的权值线段树(记录邻居中的小节点的信息,因此叶子数上限大于等于 N − 350 ≈ N N-350\approx N N−350≈N):每次操作的复杂度为 O ( M A X _ D E G ⋅ log N ) O(MAX\_DEG\cdot\log N) O(MAX_DEG⋅logN);
修改大节点的权值时,直接修改自己的权值:每次操作的复杂度为 O ( 1 ) O(1) O(1);
查询节点的 MEX 值时,需要合并自己权值线段树上的信息以及邻居中的大节点的信息,我的做法是:
每次操作的复杂度为 O ( 4 m M A X _ D E G ⋅ log N ) O(\frac{4m}{MAX\_DEG}\cdot\log N) O(MAX_DEG4m⋅logN);
因此一次操作的最大的可能复杂度为 O ( max ( M A X _ D E G , 4 m M A X _ D E G ) ⋅ log N ) O(\max(MAX\_DEG,\frac{4m}{MAX\_DEG})\cdot\log N) O(max(MAX_DEG,MAX_DEG4m)⋅logN);令 M A X _ D E G = = 4 m M A X _ D E G MAX\_DEG==\frac{4m}{MAX\_DEG} MAX_DEG==MAX_DEG4m 得 M A X _ D E G MAX\_DEG MAX_DEG 取 KaTeX parse error: Undefined control sequence: \root at position 1: \̲r̲o̲o̲t̲\of{4m}\approx6… 最佳,则一次操作的最大的可能复杂度为 KaTeX parse error: Undefined control sequence: \root at position 3: O(\̲r̲o̲o̲t̲\of{4m}\cdot\lo…。
因此总复杂度为 KaTeX parse error: Undefined control sequence: \root at position 9: O(q\cdot\̲r̲o̲o̲t̲\of{4m}\cdot\lo…, 1 0 5 ⋅ 640 ⋅ log ( 1 0 5 ) = 1.08 ∗ 1 0 9 10^5\cdot 640\cdot \log (10^5) = 1.08*10^9 105⋅640⋅log(105)=1.08∗109。(由于常数较大,上述推导保留了常数)。
(题解使用的界线是 350 350 350,然而 35 0 2 ≥ 1 ∗ 1 0 5 350^2\geq 1*10^5 3502≥1∗105,我猜测作者忽略了边数应该乘二)。
我又想到,对于查询节点 u u u 的 MEX 值的操作,还可以这样实现:
则每次查询操作的复杂度降为
O ( 2 m M A X _ D E G log 2 m M A X _ D E G + 2 m M A X _ D E G ⋅ ( log 2 m M A X _ D E G + log N ) ) = O ( 2 m M A X _ D E G ⋅ ( log 4 m M A X _ D E G + log N ) ) O(\frac{2m}{MAX\_DEG}\log\frac{2m}{MAX\_DEG} + \frac{2m}{MAX\_DEG}\cdot(\log\frac{2m}{MAX\_DEG}+\log N)) \\=O( \frac{2m}{MAX\_DEG}\cdot(\log\frac{4m}{MAX\_DEG}+\log N)) O(MAX_DEG2mlogMAX_DEG2m+MAX_DEG2m⋅(logMAX_DEG2m+logN))=O(MAX_DEG2m⋅(logMAX_DEG4m+logN))
更难以继续分析,放弃。
参考资料: https://www.cnblogs.com/dysyn1314/p/13357864.html
本章是对上述博客的整理。
对于单点修改、区间查询问题,如果瓶颈出现在修改(即修改次数较多),可以考虑把线段树改成分块。根据第二、三章的分析,可对每个节点维护一套分块,存储其邻居的权值出现情况,且每个节点 u u u 的分块占用空间只需 deg u + 1 \deg_u+1 degu+1。
具体来说,对每个结点 u u u,维护两个 vector
(C++)(总共就需要两个 vector
数组):
cnt[u][x]
表示节点 u u u 有多少个邻居的权值为 x x x,显然 cnt[u]
的第二维大小为 deg u + 1 \deg_u+1 degu+1;block_cnt[u][x/B]
表示对 cnt[u]
以 B B B 为子块大小进行分块后,第 ⌊ x B ⌋ \lfloor\frac{x}{B}\rfloor ⌊Bx⌋ 块中(记录 [ ⌊ x B ⌋ , ⌊ x B ⌋ + B − 1 ] [ \lfloor\frac{x}{B}\rfloor , \lfloor\frac{x}{B}\rfloor+B-1 ] [⌊Bx⌋,⌊Bx⌋+B−1] 内的非负整数情况)有多少个数出现过;那么,修改一个邻居的复杂度就降至 O ( 1 ) O(1) O(1) 了,只需增减 cnt[][]
的值,并在其增至 1 1 1 或减至 0 0 0 时对 block_cnt[][]
进行增减。
接下来研究如何应对第二章第三段提出的特别极端的可能输入数据。
设分块的子块大小为 B B B、那么块数为 O ( max ( deg u + 1 ) B ) = O ( N B ) O(\frac{\max(\deg_u+1)}{B})=O(\frac{N}{B}) O(Bmax(degu+1))=O(BN);设我们选出的大节点的个数为 C C C、那么小节点的最大度数为 O ( N C ) O(\frac{N}{C}) O(CN)。我们的策略是
当修改小节点的权值时,直接更新其各邻居的分块: O ( N C ) O(\frac{N}{C}) O(CN);
当修改大节点的权值时,怎么更新那么多邻居节点的分块?不更新了,等到某邻居被查询前再主动更新!当然,每个节点就要再开一个 vector
记录自己分块中存储的邻居中的大节点的旧权值以方便比对、修改。 O ( 1 ) O(1) O(1)。
因此,单次修改操作复杂度为 O ( N C ) O(\frac{N}{C}) O(CN)。
另外,这篇博客的解法同样将节点分为大节点和小节点,但不再规定分块只存邻居中的小节点的权值出现情况了(而是所有邻居都存)。那么,查询时只要做两件事:
因此,单次查询操作的复杂度: ( C + N B + B ) (C+\frac{N}{B}+B) (C+BN+B)。
显然,取 KaTeX parse error: Undefined control sequence: \root at position 5: B=C=\̲r̲o̲o̲t̲\of{N} 最佳。因此,初始化 O ( N + M ) O(N+M) O(N+M)、单次修改操作 KaTeX parse error: Undefined control sequence: \root at position 3: O(\̲r̲o̲o̲t̲\of{N})、单次查询操作 KaTeX parse error: Undefined control sequence: \root at position 4: O(3\̲r̲o̲o̲t̲\of{N}),总复杂度 KaTeX parse error: Undefined control sequence: \root at position 15: O(N+M+q\cdot 3\̲r̲o̲o̲t̲\of{N})。
对于每个节点,我们要记录它邻居的权值出现信息、且这一信息动态可变,于是要记录次数。
一方面,我们发现,对于每个节点 u u u,其需要分别记录次数的权值(非负整数)不需要是 [ 0 , 1 0 9 ] [0,10^9] [0,109],而只需要 [ 0 , deg u ] [0,\deg_u] [0,degu];另一方面,在这 [ 0 , deg u ] [0,\deg_u] [0,degu] 中如何快速在其中找出 MEX,我们又使用线段树或分块进行进一步优化。
然后TLE了。我们想到,度数较大的节点的权值修改操作会大量耗时,而这类节点的数量有限。于是我们不让这些大节点去及时更新所有邻居的线段树或分块,而是让某邻居需要用到时再跑来问自己。
#include
#include
#include
//#include
const int B=350;
int A[100005];
std::vector<int> neighbors[100005];
int deg[100005];
typedef std::pair<int,int> pii;
std::vector<pii> big_neighbors[100005];
std::vector<int> cnt[100005];
std::vector<int> block_cnt[100005];
void add1(int u , int x){ // add value x once more into the data structure (storing information of neighbors) of node u.
if(x>deg[u]) x=deg[u];
++cnt[u][x];
if(cnt[u][x]==1) ++block_cnt[u][x/B];
}
void delete1(int u , int x){ // don't use name "delete"
if(x>deg[u]) x=deg[u];
--cnt[u][x];
if(cnt[u][x]==0) --block_cnt[u][x/B];
}
int main(){
int T;
scanf("%d" , &T);
while(T--){
int n,m;
scanf("%d%d" , &n , &m);
for(int i=1 ; i<=n ; ++i) scanf("%d" , &A[i]);
for(int u=1 ; u<=n ; ++u) neighbors[u].clear();
memset(deg , 0 , sizeof deg);
for(int i=0 ; i<m ; ++i){
int u,v;
scanf("%d%d" , &u , &v);
neighbors[u].push_back(v);
neighbors[v].push_back(u);
++deg[u];
++deg[v];
}
for(int u=1 ; u<=n ; ++u) big_neighbors[u].clear();
for(int u=1 ; u<=n ; ++u){
cnt[u].clear();
cnt[u].resize(deg[u]+1); // resize() set those new elements as zero
block_cnt[u].clear();
block_cnt[u].resize(deg[u]/B + 1); // not ((deg[u]+1)/B);
for(int i=0 ; i<neighbors[u].size() ; ++i){
int v = neighbors[u][i];
add1(u,A[v]);
if(deg[v]>B) big_neighbors[u].push_back( std::make_pair(v,A[v]) );
}
}
int q;
scanf("%d" , &q);
while(q--){
int ope;
scanf("%d" , &ope);
if(ope==1){
int u, x;
scanf("%d%d" , &u , &x);
if(deg[u]<=350){
for(int i=0 ; i<neighbors[u].size() ; ++i){
int v = neighbors[u][i];
delete1(v , A[u]);
add1(v , x);
}
}
A[u]=x;
}else{
int u;
scanf("%d" , &u);
// update the data structure of u
for(int i=0 ; i<big_neighbors[u].size() ; ++i){
int v = big_neighbors[u][i].first;
int old_a = big_neighbors[u][i].second;
if(old_a != A[v]){
delete1(u , old_a);
add1(u , A[v]);
big_neighbors[u][i].second = A[v];
}
}
// query MEX
for(int L=0 ; L<=deg[u] ; L+=B){
int R = L+B-1;
if(R>deg[u]) R=deg[u];
if(block_cnt[u][L/B] == R-L+1) continue;
for(int i=L ; i<=R ; ++i){
if(cnt[u][i]==0){
printf("%d\n" , i);
break;
}
}
break;
}
}
}
}
return 0;
}