O(1)判断两点之间是否有边

问题描述

给定一张 \(n\) 个点,\(m\) 条边的有向图。

多次询问,要求每次 \(\mathcal{O}(1)\) 判断两点之间是否有边(你可以忽略输入、输出等问题)。

数据范围:\(2\leq n\leq 4\times 10^5\),\(0\leq m\leq 8\times 10^5\)。

空间限制:\(512\texttt{MB}\)。

做法

朴素做法有三种:

  • 对每个点 \(u\),用一个 \(\texttt{vector}\) 存从它出发的边。将这些边按另一端点的大小排序。每次查询时,在 \(u\) 的 \(\texttt{vector}\) 里二分查找。这样单次询问的时间复杂度是 \(\mathcal{O}(\log n)\) 的。如果对每个点维护一个 \(\texttt{map}\) 或 \(\texttt{set}\),本质是一样的。
  • 用一个二维 \(\texttt{bool}\) 型数组 \(\texttt{a[u][v]}\),表示点 \(u, v\) 之间是否有边。这样单次询问时间复杂度是 \(\mathcal{O}(1)\) 的,但是空间复杂度高达 \(\mathcal{O}(n^2)\),无法承受。
  • 哈希。本文不讨论。

考虑将前两种做法结合。

设 \(x = 11\)。把每 \(2^x\) 个点分为一类。这样共有 \(\frac{n}{2^x}\) 类。用一个大小为 \(\frac{n^2}{2^x}\) 的数组,就能实现判断:每个点向每一类点之间是否有连边。

如果一个点 \(u\) 向某一类点 \(t\) 之间有连边,我们称之为一个“事件”。容易发现,事件至多只有 \(m\) 个

考虑每个事件,它对应的入点至多只有 \(2^x\) 个。将这 \(2^x\) 个点再分类。把每 \(2^6\) 个点分为一类,会分出 \(2^{x - 6}\) 类。每一类点里编号都小于 \(2^6 = 64\)。一个 \(\texttt{unsigned long long}\) 有 \(64\) 位,所以刚好可以用一个 \(\texttt{unsigned long long}\) 描述其状态。

在上述做法里,我们总共需要 \(\frac{n^2}{2^x}\) 个 \(\texttt{int}\),和 \(m\cdot 2^{x - 6}\) 个 \(\texttt{unsigned long long}\)。为了估算方便,不妨假设 \(m = 2n\)。那么所需的字节数是:\(4\cdot \frac{n^2}{2^x} + 8\cdot 2n\cdot 2^{x - 6}\),令他们相等,解得 \(x = 11\) 时该式取到最小值。刚好 \(500\texttt{MB}\) 不到。

参考代码:

const int MAXN = 4e5, MAXM = 8e5;
const int FULL5 = (1 << 5) - 1;
const int FULL6 = (1 << 6) - 1; int b1[MAXN + 5][MAXN / (1 << 11) + 5], cnt_b1;
ull b2[MAXM + 5][FULL5 + 1]; void add_edge(int u, int v) {
if (!b1[u][v >> 11]) b1[u][v >> 11] = ++cnt_b1;
b2[b1[u][v >> 11]][(v >> 6) & FULL5] |= 1ull << (v & FULL6);
}
bool have_edge(int u, int v) {
if (!b1[u][v >> 11]) return false;
return b2[b1[u][v >> 11]][(v >> 6) & FULL5] & (1ull << (v & FULL6));
}

另外,\(n\leq 2\times 10^5\),\(m\leq 4\times 10^5\) 时,上述代码只需要改变 MAXNMAXM 的值,其他参数不变,空间消耗就降到 \(171\texttt{MB}\) 了。

进一步的思考

上述做法里,我们只分了两层,这是为了介绍该算法的核心思路。其实,如果不考虑时间上的常数,我们还可以分更多层,以此来进一步优化我们的空间消耗。

例如,在 \(n\leq 10^6\),\(m\leq 2\times 10^6\) 时,如果分四层,则空间消耗仅需 \(360\texttt{MB}\)。代码如下:

const int MAXN = 1e6, MAXM = 2e6;
const int FULL3 = (1 << 3) - 1;
const int FULL6 = (1 << 6) - 1; int b1[MAXN + 5][MAXN / (1 << 15) + 5], cnt_b1;
int b2[MAXM + 5][1 << 3], cnt_b2;
int b3[MAXM + 5][1 << 3], cnt_b3;
ull b4[MAXM + 5][1 << 3]; void add_edge(int u, int v) {
if (!b1[u][v >> 15])
b1[u][v >> 15] = ++cnt_b1;
int id1 = b1[u][v >> 15]; if (!b2[id1][(v >> 12) & FULL3])
b2[id1][(v >> 12) & FULL3] = ++cnt_b2;
int id2 = b2[id1][(v >> 12) & FULL3]; if (!b3[id2][(v >> 9) & FULL3])
b3[id2][(v >> 9) & FULL3] = ++cnt_b3;
int id3 = b3[id2][(v >> 9) & FULL3]; b4[id3][(v >> 6) & FULL3] |= 1ull << (v & FULL6);
}
bool have_edge(int u, int v) {
if (!b1[u][v >> 15])
return false;
int id1 = b1[u][v >> 15]; if (!b2[id1][(v >> 12) & FULL3])
return false;
int id2 = b2[id1][(v >> 12) & FULL3]; if (!b3[id2][(v >> 9) & FULL3])
return false;
int id3 = b3[id2][(v >> 9) & FULL3]; return b4[id3][(v >> 6) & FULL3] & (1ull << (v & FULL6));
}

之所以能不断向下分层,而且使空间消耗奇迹般地减小,它的核心是:不论怎么分,每层的事件都至多只有 \(m\) 个。

把这种思路推到极致,如果分出 \(\log n\) 层,则时间复杂度将回到 \(\mathcal{O}(\log n)\),此时相当于给每个点 \(u\) 开了一个 \(\text{01-Trie}\)。

我们只需要记住,层数越多,时间上消耗越大,空间上消耗越小。本算法的精髓就是在它们之间找到符合实际需求的平衡点。

O(1)判断两点之间是否有边的更多相关文章

  1. Floyd算法——计算图中任意两点之间的最短路径

    百度百科定义:传送门 一.floyd算法 说实话这个算法是用来求多源最短路径的算法. 算法原理: 1,从任意一条单边路径开始.所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大. 2,对 ...

  2. POJ 3660 Cow Contest 任意两点之间的关系 Floyd

    题意:牛之间有绝对的强弱,给出一些胜负关系,问有多少头牛可以确定其绝对排名. #include <iostream> #include <cstdio> #include &l ...

  3. sql server2008根据经纬度计算两点之间的距离

    --通过经纬度计算两点之间的距离 create FUNCTION [dbo].[fnGetDistanceNew] --LatBegin 开始经度 --LngBegin 开始维度 --29.49029 ...

  4. C#面向对象思想计算两点之间距离

    题目为计算两点之间距离. 面向过程的思维方式,两点的横坐标之差,纵坐标之差,平方求和,再开跟,得到两点之间距离. using System; using System.Collections.Gene ...

  5. (转)c# math 计算两点之间的角度公式

    计算两点之间的角度公式是: 假设点一(X1,Y1),点二(X2,Y2) double angleOfLine = Math.Atan2((Y2 - Y1), (X2 - X2)) * 180 / Ma ...

  6. 2D和3D空间中计算两点之间的距离

    自己在做游戏的忘记了Unity帮我们提供计算两点之间的距离,在百度搜索了下. 原来有一个公式自己就写了一个方法O(∩_∩)O~,到僵尸到达某一个点之后就向另一个奔跑过去 /// <summary ...

  7. c++ 算法 栅格中两点之间连线

    屏幕划线,通过平面坐标系实现,基本组成是一个一个的点,起点为A,终点为B 本文的算法,可以实现平面栅格中,指定的A,B两点之间进行连线(代码中仅打印了两点间需要画出的坐标点) #include < ...

  8. 求两点之间距离 C++

    求两点之间距离(20 分) 定义一个Point类,有两个数据成员:x和y, 分别代表x坐标和y坐标,并有若干成员函数. 定义一个函数Distance(), 用于求两点之间的距离.输入格式: 输入有两行 ...

  9. 图上两点之间的第k最短路径的长度 ACM-ICPC 2018 沈阳赛区网络预赛 D. Made In Heaven

    131072K   One day in the jail, F·F invites Jolyne Kujo (JOJO in brief) to play tennis with her. Howe ...

随机推荐

  1. 初学Python-day8 案例2

    中奖率 1 import random 2 num = 123456 3 i = 1 4 while True: 5 win = random.randrange(100000, 999999) 6 ...

  2. NOIP模拟22

    T1d 一道垃圾贪心,写个堆优化或者桶就行 1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; ...

  3. C++ 、Qt计算时间的方法

    原文链接:https://blog.csdn.net/chy555chy/article/details/53405072 Qt计算时间的两种方法: QTime elapsed() : ms QTim ...

  4. Linux多线程实例解析

    Linux系统下的多线程遵循POSIX线程接口,称为 pthread.编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a.顺便说一下,Linux ...

  5. 该如何有效的提高C/C++语言编程能力

    很多答案都谈到算法的重要性,我的答案主要集中在C++上,只是一些个人经验. 其实我以前也有这样的困惑,感觉完了不知道怎么用.而且我也不是学计算机的,也没有从事相关工作,所以大概有十年的时间都没写什么程 ...

  6. 几个简单的文本处理工具:diff,patch,grep,cut,sort,tr

    1:文本处理工具:   1:diff and patch : diff是比较文件或者目录的不同,主要有3个用法: diff file1 file2 :比较file1和file2的不同:diff -u ...

  7. kvm 安装 windows 虚拟机

    作者:SRE运维博客 博客地址: https://www.cnsre.cn/ 文章地址:https://www.cnsre.cn/posts/211108848062/ 相关话题:https://ww ...

  8. C++ 变量声明 定义 作用域 链接性总结

    变量定义 变量的定义用于为变量分配存储空间,还可以为变量指定初始值.在一个程序中,变量有且仅有一个定义. 变量声明 用于向程序表明变量的类型和名字.程序中变量可以声明多次,但只能定义一次. 变量的类型 ...

  9. CentOS服务器的网络配置与部署

    1.系统安装与软件安装 1.1选择CentOs7.9release版本用作所研发系统部署服务器,官网以及所选择镜像为地址为:http://ftp.sjtu.edu.cn/centos/7.9.2009 ...

  10. 手把手从0到1:搭建Kubernetes集群

    搭建 k8s 集群网上很多教程,如果是手工部署或者实验环境可以直接使用 MiniKube 或者 Kind,来在本地启动简单的 Kubernetes 集群进行后面的学习即可.如果是使用 MiniKube ...