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 ...
随机推荐
- 如果你还不知道Apache Zookeeper?你凭什么拿大厂Offer!!
很多同学或多或少都用到了Zookeeper,并知道它能实现两个功能 配置中心,实现表分片规则的统一配置管理 注册中心,实现sharding-proxy节点的服务地址注册 那么Zookeeper到底是什 ...
- 【UE4 C++ 基础知识】<7> 容器——TSet
概述 TSet是一种快速容器类,(通常)用于在排序不重要的情况下存储唯一元素. TSet 类似于 TMap 和 TMultiMap,但有一个重要区别:TSet 是通过对元素求值的可覆盖函数,使用数据值 ...
- 【数据结构与算法Python版学习笔记】树——树的遍历 Tree Traversals
遍历方式 前序遍历 在前序遍历中,先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树. 中序遍历 在中序遍历中,先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树. 后序遍 ...
- 【做题记录】[NOIP2011 提高组] 观光公交
P1315 [NOIP2011 提高组] 观光公交 我们想在 \(k\) 次加速每一次都取当前最优的方案加速. 考虑怎样计算对于每一条边如果在当前情况下使用加速器能够使答案减少的大小. 如果当前到达某 ...
- 让 AI 为你写代码 - 体验 Github Copilot
前几天在群里看到有大神分享 Copoilot AI 写代码,看了几个截图有点不敢相信自己的眼睛.今天赶紧自己也来体验一下 Copoilot AI 写代码到底有多神奇. 申请 现在 Copoilot 还 ...
- Go语言实现APPID登录
package thirdparty import ( "crypto/rsa" "fmt" "github.com/dgrijalva/jwt-go ...
- glibc memcpy() 源码浅谈
其实我本来只是想搞懂为什么memcpy()函数的参数类型是void *的: 我以为会在memcpy()源码中能找到答案,其实并没有,void *只是在传递参数的时候起了作用,可以让memcpy()接受 ...
- C++ 变量声明 定义 作用域 链接性总结
变量定义 变量的定义用于为变量分配存储空间,还可以为变量指定初始值.在一个程序中,变量有且仅有一个定义. 变量声明 用于向程序表明变量的类型和名字.程序中变量可以声明多次,但只能定义一次. 变量的类型 ...
- vue3快速上手
前言 虽然Vue3肯定是未来的趋势,但还不是很成熟,实际开发中用的也不多,建议学Vue3之前先掌握Vue2,将Vue3作为未来的知识储备. Vue3快速上手 Vue3简介 2020年9月18日,Vue ...
- git远程仓库、提交代码操作
初始化仓库 1.初始化 git init #或 git clone 远程仓库地址 git init 后续要添加远程仓库,git clone不需要再添加 2.连接仓库 git remote add 远程 ...