• 高维前缀和略解


    高维前缀和略解

    \[made\ by\ Ameiyo \]


    前言

    似乎我们教练也说是叫 高维前缀和 。。。

    那么高位前缀和又是什么呢。。。

    算了这些是题外话。

    关于高维前缀和:

    目前看来可以用于 $ DP $ 的优化,但是有一定的局限性。

    若对于所有的状态 $ S $ ,其需要向 $ S' $ 贡献答案,在这里我们称 $ S' $ 为 $ S $ 的父亲, $ S $ 为 $ S' $ 的子孙。

    高维前缀和的使用条件:

    (这个条件其实无视也无所谓,因为是我自己根据证明方法胡诌的。换句话说,如果证明方法,或者说更新方法不一样,约束也就不一样了)

    (1) $ S' $ 的父亲必是其子孙 $ S $ 的父亲之一。

    (2) $ S $ 到其某个父亲的 $ S' $ 的转移路径或是直接转移,或是能通过其他直接转移一个个转移上去,并且中间的状态必是 $ S $ 的父亲。

    (3) 父子关系不能有环。

    从上述第三点也可以看出状态的转移满足无后效性,所以高维前缀和往往是对 $ DP $ 的优化。

    loop d : // 遍历所有直接转移
    	loop S : // 遍历所有状态
    		if (check(S + d)) dp(S + d) += dp(S) // 如果合法,更新
    
    

    而 $ DP $ 的做法往往是这样:

    loop S : // 遍历所有的状态
    	loop D : // 遍历所有转移,包括间接转移
    		if (check(S + d)) dp(S + d) += val(S) // 如果合法,更新
    
    

    这个代码看起来没有 $ DP $ 的样子,实际上他可能是某个 $ DP $ 的内层,这个时候优化可能就很有用了。

    对比两份代码,我们会发现高位前缀和对时间的优化主要是针对转移。相当于是把大的转移拆分成小的互不干扰的转移,这种优化实际上有一个很经典的实例—— 多重背包的二进制优化

    在理解时,可以借助二进制优化来思考


    更详细的讲解

    为了更好的理解,这里先使用较常用的二进制表示集合(状态)的方法来做例子。(没错就是刷状压时遇到的QAQ)

    先看一道题。

    CodeChef MAXOR: Good Pairs

    (这里给出的是 $ vjudge $ 链接)

    You are given $ N $ integers: $ A_1 $ , $ A_2 $, ..., $ A_N $ . You need to count the number of pairs of indices $ (i, j) $ such that $ 1 \leq i < j \leq N $ and $ A_i | A_j \leq max(A_i, A_j) $ .

    给你 $ n $ 个数,每个数是 $ A_i $ ,求有多少对 $ (i, j) $ ,满足 $ A_i | A_j \leq max(A_i, A_j) $ 。

    $ 1 \leq N \leq 10 ^ 6 $

    $ 0 \leq A_i \leq 10 ^ 6 $

    $ 1 \leq Sum\ of\ N\ over\ all\ test\ cases \leq 10 ^ 6 $

    因为 $ x\ |\ y\ \geq x $ ,所以满足 $ A_i\ |\ A_j \leq max( A_i,\ A_j ) $ 就相当于 $ A_i\ |\ A_j\ =\ max(A_i,\ A_j ) $ 。在 $ A_i \leq A_j $ 的时候就相当于 $ A[i] $ 的二进制表示法是 $ A_j $ 的子集(被包含)。

    我们用 $ f[i] $ 表示二进制表示法下是 $ i $ 的子集的数的个数,其中有 $ g[i] $ 个于 $ i $ 相同。

    那么 $ i $ 对 $ Ans $ 的贡献就是

    \[g[i] \cdot (g[i] - 1) / 2 + (f[i] - g[i]) \cdot g[i] \]

    重点就是 $ f[i] $ 的统计。

    如果是普通的枚举,那么复杂度将会达到 $ O( \Sigma C_n ^ k \cdot 2 ^ k ) $ , $ n $ 可以达到 $ 20 $,这会是 $ TLE $ 级别的复杂度(不想算。。但是在 $ k = 10 $ 的时候就已经有亿万级别了)。

    (复杂度的计算是这样的,每个状态被自己的所有子集更新一次,子集个数是 $ 2 ^ k $ ,$ k $ 是状态中 $ 1 $ 的个数。那么 $ 1 $ 的个数是 $ k $ 的状态个数是 $ C_n ^ k $ ,也就是上述的那样。在 $ k = 10,\ n = 20 $ 时,大小为 $ 189190144 $ )

    我应该没算错QAQ。

    所以我们需要用到高位前缀和。

    初始时 $ f[i] = g[i] $ ,然后

    loop i from 0 to 21 : // 枚举直接转移(通过某一位上的1)
    	loop S from Maxn to 0 : // 枚举状态
    		if (S & (1 << i)) dp(S) += dp(S ^ (1 << i)) // 如果可以更新
    
    

    注意枚举转移放在了外层循环,而不是内层,否则就会重复许多。

    下面证明这样子对于所有的 $ S $ 而言,更新到了其全部的父亲并且没有重复更新。

    这里我们建一个图,当且仅当$ S $ 能通过改变一个 $ 0 $ 为 $ 1 $ 变为 $ S' $ 时,从 $ S $ 为起点,向 $ S' $ 连边,于是一条边就对应着代码里的一次转移。

    比如说 $ 000 $ 可以向 $ 010, 001, 100 $ 连边,但是不能向 $ 011 $ 连边,因为它改变了两个 $ 0 $ 为 $ 1 $ 。

    那么,$ 3 $ 位的图就是这个样子

    3位01的树.png

    这张图也可以很好的解释为什么在内层循环枚举转移会重复,因为每次把每个状态的所有边同时走了一遍。

    可以发现在每一层的节点中, $ 1 $ 的个数都是相同的,并且上下两层呈递增关系。

    每走一条边,就对应着代码里一次更新,也就是相当于把 到达过 当前状态 $ S $ 的点累积到 $ S' $ 中。

    (用“到达过”是因为加了之后不会去掉)

    对于某个 $ S $ 及其任意一个父亲 $ S' $ ,设 $ t = S' - S $ ,也就是二者的差。

    假设 $ t $ 中 $ 1 $ 的个数为 $ cnt $ ,那么 $ S $ 需要走 $ cnt $ 条边才能到达 $ S' $ 。

    $ t $ 里面包括的 $ 1 $ 的种类(哪一位上的 $ 1 $ )是固定的,也就是说 $ S $ 只有在经过且只经过 $ t $ 里面所有的 $ 1 $ 后才会到达 $ S' $。

    比如说按照代码,三位的转移如下:

    转移的1 \ 结点 000 001 010 100
    初始 000 001 010 100
    经过001的更新 000 000,001 010 100
    经过010的更新 000 000,001 000,010 100
    经过100的更新 000 000,001 000,010 000,100
    --- --- --- --- ---
    转移的1 \ 结点 011 101 110 111
    初始 011 101 110 111
    经过001的更新 010,011 100,101 110 110,111
    经过010的更新 $ \ $ 000,001,010,011 100,101 100,110 $ \ $ 100,101,110,111
    经过100的更新 $ \ $ 000,001,010,011 $ \ $ 000,001,100,101 $ \ $ 000,010,100,110 $ \ $ 000,001,010,100,011,101,110,111

    而走的边的种类是按顺序枚举的,也就是说 $ S $ 到 $ S' $ 的路径用边来描述是一个有序数列,既然顺序固定,边的种类又没有变化,那么这个数列是唯一的。也就是说 $ S $ 按照代码只会在一种 转移的集合 中更新到 $ S' $ 。也就是不会重复更新。

    而既然每条路径都是存在的,也就是 $ t $ 是必然能通过一种序列满足的,也就是说这个数列是必然存在的,那么对于 $ S $ 所有的父亲而言,对应的 $ t $ 都是有对应的 符合代码的转移路径 的,即所有的父亲都会更新到。

    个人认为可能有一点抽象,还不懂得同学可以手动模拟一下,记录当前状态下的 $ dp $ 数组包含了那些点,然后顺着每次挑一种边累加下去。。。(可以对着上面的表格)

    这种模拟的方法可以帮助你很大程度上理解算法,然后看证明可能会更容易看懂。如果还没有看懂可以 $ QQ $ (1134406557)找我。

    然后回到这道题。

    解析前面也说的很详细了,裸的高维前缀和就行,直接上代码吧。

    n = read<int>();
    for (reg int i = 1, x; i <= n; ++i) {
    	x = read<int>();
    	++f[x], ++g[x];
    }
    for (reg int i = 0; i < 21; ++i)
    	for (reg int j = 0; j <= N - 5; ++j)
    		if (j & (1 << i)) f[j] += f[j ^ (1 << i)];
    ll Ans = 0;
    for (reg int i = 0; i <= N - 5; ++i) {
    	if (g[i]) {
    		Ans += g[i] * (g[i] - 1) / 2;
    		Ans += g[i] * (f[i] - g[i]);
    	}
    }
    cout << Ans << endl;
    	
    

    (注意多组数据别忘记清零)


    条件的进一步提炼

    这里再来看一看高维前缀和使用的条件,借助证明把他提取出来。

    我们会注意到证明中很重要的一点,那就是 转移的路径 。事实上,只要保证了按照代码的运行,转移的路径 只有一条并且一定存在 就满足了必定更新与不重复更新,也就保证了答案的正确性。

    (事实上就是对上述三条的总结QAQ)

    无环就是显然的只有一条路径的必要条件,也即无后效性。

    也就是说,高维前缀和的使用与否可能是取决于代码,甚至是对数组意义的定义。


    更多的例题

    目前找到的题目比较少。。。不是和上一道题类似就是涉及到没学过的算法,或许等老师之后给我们开专题后会加上来。

    (说白了就是我太菜了嘤嘤嘤)

  • 相关阅读:
    动手搭建 Web 框架深入了解 Web 框架的本质
    HTTP 协议
    Objectarx 判断点是否在多段线内部
    最小面积圆覆盖
    最小面积矩形覆盖
    objectarx之(点集)凸包算法
    Objectarx之相交矩形求并集 面域转多段线
    实时监控linux主机网络活动,上传下载速度
    block SSH attacks on Linux with denyhosts (centOS7)
    ASCII Camera
  • 原文地址:https://www.cnblogs.com/Ameiyo/p/11597697.html
Copyright © 2020-2023  润新知