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


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

    问题描述

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

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

    数据范围:(2leq nleq 4 imes 10^5)(0leq mleq 8 imes 10^5)

    空间限制:(512 exttt{MB})

    做法

    朴素做法有三种:

    • 对每个点 (u),用一个 ( exttt{vector}) 存从它出发的边。将这些边按另一端点的大小排序。每次查询时,在 (u)( exttt{vector}) 里二分查找。这样单次询问的时间复杂度是 (mathcal{O}(log n)) 的。如果对每个点维护一个 ( exttt{map})( exttt{set}),本质是一样的。
    • 用一个二维 ( exttt{bool}) 型数组 ( exttt{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)。一个 ( exttt{unsigned long long})(64) 位,所以刚好可以用一个 ( exttt{unsigned long long}) 描述其状态。

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

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

    进一步的思考

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

    例如,在 (nleq 10^6)(mleq 2 imes 10^6) 时,如果分四层,则空间消耗仅需 (360 exttt{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) 开了一个 ( ext{01-Trie})

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

  • 相关阅读:
    Android ListView常用用法
    android ListView详解
    /使用匿名内部类来复写Handler当中的handlerMessage()方法
    android Handler的使用(一)
    Android之Handler用法总结
    动态设置android:drawableLeft|Right|Top|Bottom
    Android Drawable Resource学习(十)、ScaleDrawable
    Android开发——关于onCreate的解读
    onCreate()方法中的参数Bundle savedInstanceState 的意义用法
    Android之drawable state各个属性详解
  • 原文地址:https://www.cnblogs.com/dysyn1314/p/14438199.html
Copyright © 2020-2023  润新知