2020杭电暑期多校01 06 - Finding a MEX (HDU6756) 分块

20200721005922

2020杭电暑期多校01 06 - Finding a MEX (HDU6756) 分块

一、题意

给定一个 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; T10;n105;m105;q105;109;

(注:本文中的节点数 n n n、边数 m m m 均不区分大小写,大小写混用了……)

二、赛上TLE做法(优化不足够)

首先我们可以对每个节点 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 0i109)维护的是权值 i i i 出现了几次,线段树区间的含义是对应叶子所代表权值的出现次数的最小值。这样,

  • 修改一个节点 u u u 的权值时,只需从其所有邻居各自的权值线段树中对原权值的次数减一、对新权值的次数加一。
  • 查询一个节点 u u u 的 MEX 值时,在它自己的权值线段树上逐层向下,每层当左子区间 [ l , m i d ] [l,mid] [l,mid] 线段树值为 0 0 0(表示此区间内对应叶子所代表权值的出现次数的最小值为 0 0 0,即存在某权值 l ≤ v a l u e ≤ m i d l\leq value\leq mid lvaluemid 出现次数为零、因而不在 S u S_u Su 中)就只进入左子区间内查找、否则就只进入右子区间内查找。

(上面算是权值线段树基础知识)

然而,我们发现,每个节点 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=uVdegu (无向图的性质),叶子总数为 ∑ 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 uV(degu+1)=2m+n3105,则线段树只需开 1.2 ∗ 1 0 6 1.2*10^6 1.2106 个节点。

赛上已经考虑到,即使这样做仍有可能超时:如果其中 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 uVdegu=2m2105,因此度数大于 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 N350N):每次操作的复杂度为 O ( M A X _ D E G ⋅ log ⁡ N ) O(MAX\_DEG\cdot\log N) O(MAX_DEGlogN)​;

  • 修改大节点的权值时,直接修改自己的权值:每次操作的复杂度为 O ( 1 ) O(1) O(1)

  • 查询节点的 MEX 值时,需要合并自己权值线段树上的信息以及邻居中的大节点的信息,我的做法是:

    1. 先逐个查询邻居中的大节点的信息,将各值临时存入自己的权值线段树: O ( 2 m M A X _ D E G ⋅ log ⁡ N ) O(\frac{2m}{MAX\_DEG}\cdot\log N) O(MAX_DEG2mlogN)
    2. 在自己的权值线段树(同上,叶子数约为 N N N)上查询: O ( log ⁡ N ) O(\log N) O(logN)
    3. 再逐个查询邻居中的大节点的信息,把临时存入的各值从自己的权值线段树上删去: O ( 2 m M A X _ D E G ⋅ log ⁡ N ) O(\frac{2m}{MAX\_DEG}\cdot\log N) O(MAX_DEG2mlogN)

    每次操作的复杂度为 O ( 4 m M A X _ D E G ⋅ log ⁡ N ) O(\frac{4m}{MAX\_DEG}\cdot\log N) O(MAX_DEG4mlogN)

因此一次操作的最大的可能复杂度为 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_DEGKaTeX 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 105640log(105)=1.08109​。(由于常数较大,上述推导保留了常数)。

(题解使用的界线是 350 350 350,然而 35 0 2 ≥ 1 ∗ 1 0 5 350^2\geq 1*10^5 35021105,我猜测作者忽略了边数应该乘二)。

我又想到,对于查询节点 u u u 的 MEX 值的操作,还可以这样实现:

  1. u u u 自己的线段树上查询,得到只考虑邻居中的小节点时的 MEX 值,记为 a n s ans ans
  2. 将邻居中的大节点的权值排序,方便后续比对;
  3. 将当前答案 a n s ans ans 与邻居中的大节点的权值作比较,如果 a n s ans ans 出现了,则将线段树权值搜索范围缩小至 [ a n s + 1 , deg ⁡ u ] [ans+1,\deg_u] [ans+1,degu]
  4. 重复步骤3,直至找到答案;

则每次查询操作的复杂度降为
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+B1] 内的非负整数情况)有多少个数出现过;

那么,修改一个邻居的复杂度就降至 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)

另外,这篇博客的解法同样将节点分为大节点和小节点,但不再规定分块只存邻居中的小节点的权值出现情况了(而是所有邻居都存)。那么,查询时只要做两件事:

  • 先更新大邻居的信息: O ( C ) O(C) O(C),其中 C C C 为(被我们选出的)大节点的个数;
  • 再在分块中查询: O ( N B + B ) O(\frac{N}{B}+B) O(BN+B)

因此,单次查询操作的复杂度: ( 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了。我们想到,度数较大的节点的权值修改操作会大量耗时,而这类节点的数量有限。于是我们不让这些大节点去及时更新所有邻居的线段树或分块,而是让某邻居需要用到时再跑来问自己。

六、AC代码

#include<cstdio>
#include<vector>
#include<string.h>
//#include<utility>

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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值