O(1)判断两点之间是否有边
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\) 时,上述代码只需要改变 MAXN
和 MAXM
的值,其他参数不变,空间消耗就降到 \(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)判断两点之间是否有边的更多相关文章
- Floyd算法——计算图中任意两点之间的最短路径
百度百科定义:传送门 一.floyd算法 说实话这个算法是用来求多源最短路径的算法. 算法原理: 1,从任意一条单边路径开始.所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大. 2,对 ...
- POJ 3660 Cow Contest 任意两点之间的关系 Floyd
题意:牛之间有绝对的强弱,给出一些胜负关系,问有多少头牛可以确定其绝对排名. #include <iostream> #include <cstdio> #include &l ...
- sql server2008根据经纬度计算两点之间的距离
--通过经纬度计算两点之间的距离 create FUNCTION [dbo].[fnGetDistanceNew] --LatBegin 开始经度 --LngBegin 开始维度 --29.49029 ...
- C#面向对象思想计算两点之间距离
题目为计算两点之间距离. 面向过程的思维方式,两点的横坐标之差,纵坐标之差,平方求和,再开跟,得到两点之间距离. using System; using System.Collections.Gene ...
- (转)c# math 计算两点之间的角度公式
计算两点之间的角度公式是: 假设点一(X1,Y1),点二(X2,Y2) double angleOfLine = Math.Atan2((Y2 - Y1), (X2 - X2)) * 180 / Ma ...
- 2D和3D空间中计算两点之间的距离
自己在做游戏的忘记了Unity帮我们提供计算两点之间的距离,在百度搜索了下. 原来有一个公式自己就写了一个方法O(∩_∩)O~,到僵尸到达某一个点之后就向另一个奔跑过去 /// <summary ...
- c++ 算法 栅格中两点之间连线
屏幕划线,通过平面坐标系实现,基本组成是一个一个的点,起点为A,终点为B 本文的算法,可以实现平面栅格中,指定的A,B两点之间进行连线(代码中仅打印了两点间需要画出的坐标点) #include < ...
- 求两点之间距离 C++
求两点之间距离(20 分) 定义一个Point类,有两个数据成员:x和y, 分别代表x坐标和y坐标,并有若干成员函数. 定义一个函数Distance(), 用于求两点之间的距离.输入格式: 输入有两行 ...
- 图上两点之间的第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 ...
随机推荐
- SpringCloud-SpringBoot-SpringCloudAlibaba对应版本选择
一.SpringCloud-SpringBoot 对应的版本选择 SpringCloud官网常规方式只能查看最新的几个版本信息 https://spring.io/projects/spring-cl ...
- WPF 排版基础
一.WPF 排版基础 WPF使用控制面板来进行排版,控制面板实际上是一种可以放入WPF界面元素的容器.当用户把界面元素放入控制面板后,WPF会自动把这些界面元素放在它认为合适的地方.WPF开发人员需要 ...
- RogrePirates Scrum Meeting 博客汇总
RogrePirates 博客目录 一.Scrum Meeting 1.Alpha阶段 第一次会议 第二次会议 第三次会议 第四次会议 第五次会议 第六次会议 第七次会议 第八次会议 第九次会议 第十 ...
- BUAA SE | 提问回顾与个人总结
项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 提问回顾与个人总结 我在这个课程的目标是 深入理解软件工程 这个作业在哪个具体方面帮助我实现目标 ...
- OO第四单元
OO第四单元总结 第四单元架构设计 第一次作业 uml类图 这次作业我采取的基本思路就是根据指令来建造一个简易的类图,用于查询,其中umlclass中包含了umlAttraibute,umlOpera ...
- OO前三次作业思考(第一次OO——Blog)
OO前三次作业总结 基于度量分析程序结构 由于三次作业较多,决定分析内容.功能最为复杂的第三次作业. 上图为第三次作业的类图.我使用了一个抽象类Factor,写了五个因子继承Factor,然后又单独开 ...
- 一文读懂Android进程及TCP动态心跳保活
一直以来,APP进程保活都是 各软件提供商 和 个人开发者 头疼的问题.毕竟一切的商业模式都建立在用户对APP的使用上,因此保证APP进程的唤醒,提升用户的使用时间,便是软件提供商和个人开发者的永恒追 ...
- USART 硬件流控
流控的概念源于 RS232 这个标准,在 RS232 标准里面包含了串口.流控的定义.大家一定了解,RS232 中的"RS"是Recommend Standard 的缩写,即&qu ...
- 攻防世界 杂项 9.a_good_idea
题目描述: 汤姆有个好主意 解题思路: 首先按照隐写思路找了一下没找到flag,接着使用winhex打开图片,发现图片里面又包含了一张图片,然后马上改了一下后缀为zip, 解压后发现里面有:hint. ...
- Celery Task(定时任务)及参数
celery beat 是一个调度器:它以常规的时间间隔开启任务,任务将会在集群中的可用节点上运行. 默认情况下,入口项是从 beat_schedule 设置中获取,但是自定义的存储也可以使用,例如在 ...