跳转至

Algorithms

Data Structure Summary

导言

ASCYLIB (with OPTIK) is a concurrent data-structure library. It contains over 40 implementations of linked lists, hash tables, skip lists, binary search trees (BSTs), queues, priority queues, and stacks. ASCYLIB contains sequential, lock-based, and lock-free implementations for each data structure.

Concurrent-AMAT, C-AMAT

并发式平均存储访问时间模型

APC

侧重于测量方法的研究, 给出了并发存储的测量方法和尺度。

在 APC 中, 周期是存储活动周期 (memory active cycle), 不是通用的 CPU 周期,所以 APC 也叫 APMAC( 存储活动周期平均访问数, access per memory active cycle)。

同时 APC 采用重叠 (overlapping) 的访存时间统计方法 : 在有两个或多个存储访 问同时进行时, 周期只增加一次。

HPL.dat file detailed explanation

HPL.dat

HPL.dat
HPLinpack benchmark input file
Innovative Computing Laboratory, University of Tennessee
HPL.out      output file name (if any) 输出文件名
6            device out (6=stdout,7=stderr,file)

1            # of problems sizes (N) = sqrt((Memory Size in Gbytes * 1024 * 1024 * 1024) /8) * ratio
11136         Ns 矩阵规模

1            # of NBs block sizes (64~512)In the [96,104,112,120,128, …, 256] range; the multiple of 64
96           NBs 矩阵分块方法

0            PMAP process mapping (0=Row-,1=Column-major) 选择处理器阵列是按列的排列方式还是按行的排列方式。

1            # of process grids (P x Q)
2            Ps # two-dimensional block-cyclic data distribution = amount of processes; P≤Q
2            Qs 二维处理器网格(P×Q)

16.0         threshold 阈值

1            # of panel fact # 后面是L分解的方式
2            PFACTs (0=left, 1=Crout, 2=Right)
1            # of recursive stopping criterium
4            NBMINs (>= 1)
1            # of panels in recursion
2            NDIVs
1            # of recursive panel fact.
1            RFACTs (0=left, 1=Crout, 2=Right)
1            # of broadcast
1            BCASTs (0=1rg,1=1rM,2=2rg,3=2rM,4=Lng,5=LnM)
1            # of lookahead depth
1            DEPTHs (>=0)
2            SWAP (0=bin-exch,1=long,2=mix)
64           swapping threshold
0            L1 in (0=transposed,1=no-transposed) form
0            U  in (0=transposed,1=no-transposed) form
1            Equilibration (0=no,1=yes)
8            memory alignment in double (> 0)

合适的参数参考

Leetcode

训练

  • 简单、中等、困难在10、20、30分钟解决(倒计时计数
  • 题解在5、10、20分钟内理解,明白核心考点和解题思想,然后重写。
  • ACM模式练习

ACM输入处理

  • 基础cin如何读取字符,读取字符串
  • 接受一个字符串,遇“空格”、“TAB”、“回车”都结束
#include <iostream>
using namespace std;
int main(void)
{
    int a, b;
    while (cin >> a >> b) { }// 读到EOF结束
    while (cin) { // 读到EOF结束
      char ch = cin.get() //读取一个字符
    }
    return 0;
}

其余处理

//接受一个/多个字符
char ch; 
ch=cin.get();               //或者cin.get(ch); 
char a[20]; 
cin.get(a,20);            //可以接收空格


//getline() // 接受一行字符串,可以接收空格并输出
string str; 
getline(cin,str);  // #include<string> 
````

### cout输出处理

```c++
#include <iomanip> 
// 也可以使用 #include <bits/stdc++.h>
int main(void){
    int a = 255;
    cout << hex << a << endl;//以十六进制的形式输出
    cout << dec << a << endl;//以十进制的形式输出
    cout << oct << a << endl;//以八进制的形式输出
    //结果分别是ff 255 377
    return 0;
}
int main(void){
    double x = 3.141592658;

    //控制输出的宽度,就是printf("%10d");
    cout << setw(10) << x << endl;

    //限制输出有效数字的个数是5
    cout << setprecision(5) << x << endl;

    //fixed表示控制小数点后面的数字,两个连用就是小数点后面保留三位小数
    cout << fixed << setprecision(3) << x << endl;

    //输出结果分别是   3.14159  3.1416  3.142
    return 0;
}

考前准备

  • 弱点:
  • 字符串处理相关函数
    • 滑动窗口(有窗口的需求,如子数组;有滑动的判定)
    • 固定一端使得种类数能分类讨论
    • KMP
  • 图算法
    • 最短路径实现
    • 拓扑排序
  • 动态规划

遇题对策

  1. 题意的准确理解
  2. 构造小样例,分析题意
  3. 题目的抽象
  4. 提取出独立的不可分子问题
  5. 题目的转化
  6. 从问题的反面,所求值的补集,使用不同的变量讨论情况
  7. 通过规律(数学规律)转换问题: 可以先从小例子分析,然后猜想验证
  8. 对于输入量多的题型,必定是以下几种情况
  9. 每次操作(query)是独立的,简单分解成独立任务即可
  10. 使用特殊数据结构(单调栈,优先队列),边读边算边丢弃
  11. 读取后,处理成精简表示:
    1. 前缀和(子数组的元素和转换成两个前缀和的差, 注意为了sum[0]=0,设置sum[i]=A[0]+...A[i-1])
    2. 差分数组:B[i]=A[i]-A[i-1]
  12. 可以接受的高时间复杂度操作
    1. O(nlogn) 快排 10^5元素
  13. 题型的分类判断
  14. 常见的题型和考察知识点
  15. 陌生的技巧题
    1. 数学技巧题:图论、数值计算技巧
  16. 常见的错误
  17. 特殊输入忘记分析:0 或者 相等
  18. 边界判断错误以及越界:
    1. 数组越界, 数组下表总是写错一位
    2. for循环,或者判断的等于号缺失
    3. int 存储不下 需要long long
  19. 确实少考虑了某个边界的分析
    1. 中 2749. 得到整数零需要执行的最少操作数
  20. 循环调用太多层,导致函数栈溢出
    1. AddressSanitizer: stack-overflow on address 0x7ffe1ea87fc8 (pc 0x00000034e77b bp 0x7ffe1ea88060 sp 0x7ffe1ea87fc0 T0)
    2. 记录遍历过的点,树上记录father,图上记录遍历过点
  21. 自动补全导致的错误
    1. minmax的两行都写成min
    2. 两层循环的i,j 有处写混了
    3. memset() size*n 导致堆栈上的数据全部变成0,程序访问无法访问的地址,gdb也无法执行,变量数值也完全错误
  22. 其余
    1. *(1/2)不是*0.5*0

编程建议

  • 先注释理清思路,比如动态规划的下表,如何转移
  • 先注释写好想到的可能的forget的情况,防止写着写着忘记了。

快速debug

  • 除开肉眼快速排查上面的常见错误
  • vscode实机操作
  • 配置好头文件,和main函数
  • 找到错误样例的最小集合作为输入
  • vscode gdb

注意事项

  1. 以思路清晰简洁的代码解答优先,而不是追求O(n)的算法,导致解法复杂化
  2. 多用辅助变量来理清思路,编译器会优化,不会影响性能。
  3. 辅助数据结构的选择一定要切合题意。
  4. 存储中间数据,或者标记已经处理的情况(DP时记录hit值的次数,而不是是否hit,以便回退)
  5. 利用二维数组精简讨论的代码int direction[8][2] = {{2,1}, {2,-1}, {-2,1}, {-2,-1}, {1,2}, {1,-2}, {-1,2}, {-1,-2}};
  6. 计算时间复杂度
  7. 按照题目的上限计算

常用写法和技巧

二分答案、二分法

个人最爱写法

int maxR = *max_element(inputList.begin(), inputList.end());
 int left = 0, right = maxR;
 // 该方法的问题:默认right是不满足的,需要提前额外判断
 if(available(right)) return right;
 while(left + 1 < right) { 
  //由于无论是从left,right相差1、2、3、……开始
  //最终都会经过 left+1=right的阶段
  int mid = left + ((right-left)>>1); // 注意括号 >> 优先级最低
  long long tmpCntSum = 0;
  for(auto &x: inputList){
   tmpCntSum += (x>mid)? mid : x;
  }
  if(tmpCntSum <= cnt){
   left = mid; // while结束时 left 满足 其tmpCntSum <= cnt
  }else{
   right = mid; // while结束时 right 满足 其tmpCntSum > cnt
  }
 }
 //求 满足求和不大于cnt 的最大值:left
 return left;

其他写法

while (l < r) {
    int mid = (l + r + 1) >> 1;
    if (check(mid)) {
        l = mid;
    } else {
        r = mid - 1;
    }
}

二维遍历边界小技巧

int dirs[4][2] = {1,0,-1,0,0,1,0,-1}; 

auto check = [&](int x, int y){
    return x >= 0 && y >= 0 && x < n && y < m;
   };
for(auto &[addx, addy]: dirs){
  int nextx = x + addx;
  int nexty = y + addy;
  if(!check(nextx, nexty)) continue;
  ……
}

单调栈写法

stack<int> stk;
while (head) {
    while (!stk.empty() && stk.top() <= head->val)
        stk.pop(); // 保持从底到top单减


    stk.push(head->val); // 每个元素要加入
    head = head->next;
}

//vector 也行
vector<pair<int, int>> st; // 当前行的单调栈
for (int j = n - 1; j >= 0; --j) {
  while (!st.empty() && mn <= st.back().first)
    st.pop_back();
  st.emplace_back(mn, j);
}

常用技巧

  • iota构建用于排序的索引数组
  • itoa 是 希腊语的第九个字母,读[aɪ’otə]这个名称是从APL编程语言借鉴过来的。
  • 题目iota(id, id + n, 0); sort(id, id + n, [&heights](const auto &i, const auto &j) {return heights[i] > heights[j];}); 注意[&heights]的lambda表达式引用。
  • 快慢指针,原地去重/合并
  • 易 26. 删除有序数组中的重复项
  • 贪心枚举,确定枚举一个变量,从而贪心确定另一个。(不用双重循环)
  • 易 LCP 33. 蓄水
    • 转换思路,枚举另一维度的元素。

常见题型的框架、解法以及例题

模拟题

特征:明显的顺序处理数据O(1)空间O(1)操作

特征:有限个数集合使得时间复杂度降维

有限二维暴力题

贪心

  • 往往贪最大,和优先队列(最大堆)结合
  • 中 1054. 距离相等的条形码
    • 也可以,统计数量之后,先排偶数位,然后排奇数位。
  • 证明贪心的正确性:常用反证法
  • 假如不是贪心的方式成立,会转换或者有什么问题
  • 中 2517. 礼盒的最大甜蜜度:二分答案的子问题中,要证明打包方法,必包括两端的糖果。反证:假如情况不包含两端的糖果,该情况的两端必定可以向外扩展到两端。

考察数据结构

  • 抽象表示/题目特征(用于高效匹配题型)
  • 栈:输入数据量多,顺序push,逆序pop处理,后进先出
  • 哈希表:常见记录元素次数 ++map_cnt[x]
  • 常见的解法流程
  • 解法难点:
  • 存储元素不是简单的int,而是需要额外index信息的pair<int,int>
  • 相关拓展考点/难度延伸
  • 相关例题以及核心考点
  • 中 1019 链表中的下一个更大节点
    • 考察带额外信息的单调栈

常见困难题: 二维图带障碍的路径分析

特征:字符串相关

  • 字符串相关概念
  • 子串与子序列:子串要连续;子序列不需要连续,但是相对位置不改变。
  • 真后缀: 指除了 S 本身的 S 的后缀
  • 回文串:正着写和倒着写相同的字符串
  • 字符串基本处理
  • ASCII码 A在65 a在97
  • 大小写转换
    • char: str[i] = tolower(str[i]);//把大写全部转为小写
    • string, 第三个参数是转换之后,输出到原str字符串的起始地址。转换操作,可以选择toupper,tolower。
    • c++ string str="how are you"; transform(str.begin(),str.end(),str.begin(),::toupper);
  • 快速判断子字符串相同
  • 伪难 1147. 段式回文:暴力双指针匹配能通过O(n^2),官方对字符串进行滚动哈希预处理变成O(n)
  • 难 1163. 按字典序排在最后的子串
    • 核心思想:只有后缀子字符串才是候选的最大字典序子字符串
    • python 的切片操作LC容忍度比较高
    • 指针 i 指向已知的最大后缀子字符串,j 指向待比较的后缀子字符串
    • 一个个比较时,失败时按道理是j++, 不一定是j+k+1
    • 因为当 s[i+k]<s[j+k]时,s[j+1~j+k]内可能有更大子字符串,如cacacb字符串,比较cacacacb时,后者有更大字串cb
    • 但是通过如下关系能跳过大部分重复的区域
    • s[i+k]<s[j+k]时,跳过了以 s[i,..,i+k] 为起始位置的所有后缀子串,因为它们的字典序一定小于对应的 s[j,..,j+k] 为起始位置的后缀子串(由于s[i+k]<s[j+k])。
      • s[i,..,i+k] < s[j,..,j+k],这时j = max(j + 1, i + k + 1);
    • s[i+k]>s[j+k]时, 以s[i] > s[i+1,..,i+k] > s[j,..,j+k], 这时j=j+k+1
  • KMP算法:
  • 一种线性时间复杂度的字符串匹配算法,可以在一个字符串S内高效地定位模式字符串P的出现位置。
  • 思想:核心就是每次匹配过程中推断出后续完全不可能匹配成功的匹配过程,从而减少比较的趟数。
  • next数组实质上就是找出模式串中前后字符重复出现的个数,为了能够跳跃不可能匹配的步骤。next数组的定义为:next[i]表示模式串A[0]至A[i]这个字串,使得前k个字符等于后k个字符的最大值,特别的k不能取i+i,因为字串一共才i+1个字符,自己跟自己相等毫无意义。
  • next数组的信息,可以不用从头开始匹配,而是从重复位置开始匹配(类似双指针,或者滑动窗口的效果)

特征:图相关

  • 图论相关概念
  • 简单图:图中无自环与重边,反之为多重图
    • 自环:某边的两个端点都是同一点形成的环
  • 补图:对于无向简单图G,其补图中的边为G中边的补集
  • 图的表示
  • 数据以稠密图为主,
    • 有边权时:邻接矩阵vector<vector<int>> g;
    • g = vector<vector<int>>(n, vector<int>(n, INT_MAX / 2));
    • g[e[0]][e[1]] = e[2]; // e[2]权重
    • 无边权时:权重为1
  • 数据以稀疏图为主,使用
    • 有边权时:邻接表vector<vector<pair<int, int>>> m_adj;
    • m_adj[edge[0]].emplace_back(edge[1], edge[2]); // edge[2]权重,
    • 无边权时:邻接堆unordered_set<int> nes[n];
    • g[edge[0]].insert(edge[1]); g[edge[1]].insert(edge[0]);
    • 或者邻接表vector<vector<int>> g(n);
      • g[edge[0]].push_back(edge[1]); g[edge[1]].push_back(edge[0]);
  • 图中最短环
  • 难 2608. 图中的最短环
    • 先把图变成n点的邻接表vector<vector<int>> g(n);
    • 寻找最短,明显是从原点开始的BFS,由于要记录遍历的每个点的index和父亲节点,防止邻接表的BFS逆向,采用queue<pair<int,int>>
    • 环的判断,BFS过程中x如果有邻居y已经被遍历到,环大小为该层最小dist[x]+dist[y]+1
  • 图中最短路径
  • 难 2642. 设计可以求最短路径的图类
    • 虽然是单向路径,但是不保证无环。暴力回溯遍历,需要记录已经经过的点。虽然暴力法任然会超时
    • Dijkstra算法
    • 初始化: start距离0,其余无穷
    • 迭代步:
      • 当前点集V里距离最小的点x,就是该点到start的最小距离。find x for min(dis[x]) in V(N)或者int x = min_element(dis, dis + N) - dis;
      • 更新该点x邻居y的最短距离for (int y = 0; y < n; ++y) dis[y] = min(dis[y], dis[x] + g[x][y]); // g为邻接矩阵
      • 将x从V里剔除(已经找到)
    • 结束:
      • x为end,提前结束
      • x内容为无穷,说明其余的点start无法访问到。
    • Floyd 的以中间节点讨论的动态规划算法
    • 在一般图上,求单源最长(短)路径的最优时间复杂度为 O(nm)(Bellman-Ford 算法,适用于有负权图)或 O(m * log m)(Dijkstra 算法,适用于无负权图)。
    • 但在 DAG 上,我们可以使用 DP 求最长(短)路,使时间复杂度优化到 O(n+m)
  • 拓扑排序 - Kahn 算法
  • 初始状态下,集合 S 装着所有入度为 0 的点(一般是队列,如果题目要求字典序之类,可以换成最大堆/最小堆实现的优先队列),拓扑排序结果L 是一个空列表。
  • 每次从 S 中取出一个点 u(可以随便取)放入 L, 然后将 u 的所有边\( (u, v_1), (u, v_2), (u, v_3) \cdots \)删除。对于边 (u, v),若将该边删除后点 v 的入度变为 0,则将 v 放入 S 中。
  • 不断重复以上过程,直到集合 S 为空。检查图中是否存在任何边,如果有,那么这个图一定有环路,否则返回 LL 中顶点的顺序就是拓扑排序的结果。
  • 华为230426机试一
    • 双队列BFS。(S为空时,还有入度不为0的点,有环
    • 或者往大更新每点的距离,返回最大值。(N次后还能更新点,说明有环

特征:树相关

  • 基本概念:区分二叉搜索树,平衡二叉树
  • 树具有相同的子问题结构,常用dfs递归
  • 中 6419. 使二叉树所有路径值相等的最小代价
  • 循环删除树上叶子节点
  • 难 2603. 收集树中金币
    • 思路1:删除叶子节点
    • 先把图变成n点的邻接表,但是由于要删除使用unordered_set<int> nes[n];
    • 叶子节点的判断nes[i].size() == 1, 删除叶子节点可能会导致新的叶子产生需要queue保存循环处理
    • 思路2:拓扑排序(基于Kohn算法:从多个入度为0的叶子节点开始BFS,取出ready的点,更新其他节点的度。但是每个节点有依赖度的属性dig[i],只有节点满足依赖才能入队继续BFS)

特征:每步k分支,穷举所有答案组合

  • 抽象表示/题目特征(用于高效匹配题型)
  • 回溯法
  • 常见的解法流程
  • 采用辅助数据结构记录选择某分支导致的影响次数,以便回退
  • 解法难点:
  • 时间复杂度较高,往往需要用动态规划等替代。迫不得已不要用。
  • 相关拓展考点/难度延伸
  • 相关例题以及核心考点
  • 中 2597. 美丽子集的数目
  • 中 6899. 达到末尾下标所需的最大跳跃次数
    • 回溯的时间复杂度过高k^1000。动态规划大约1000*k

BFS

  • BFS时,一般不经过重复节点,需要记录。
  • 原因:每个节点只有位置信息时,重复遍历节点肯定不满足最小步数的要求(至少加一)。可能导致死循环。
  • BFS每步的内容,或者记录的重复节点的内容: 一般不仅有位置,还有方向等其他信息。
  • 多源BFS + 并查集/二分答案
  • 要点:区分已经遍历和未经过的区域。如果不需要存储额外信息一个queue就能实现,因为swap(tmp,cur_queue)耗时。
  • ROI: 将两点连线通过BFS的属性值变成并查集,判断在集合内

特征:每步k分支,求解min步数

  • 抽象表示/题目特征(用于高效匹配题型)
  • BFS: 每步k分支,关心步数,每个分支互不影响
  • 常见的解法流程
  • 采用双队列,每个元素存储了每个节点所需信息,每次交换相当于树的遍历更深一层。
  • 解法难点:
  • 具体根据题目简化每个节点处理的时间复杂度
    • 记录候选集合,遍历后删除,避免重复访问
  • 相关拓展考点/难度延伸
  • 相关例题以及核心考点
  • 难 2612. 最少翻转操作数
    • 由于每个节点都要对大小为n的bannedSet判断,每次O(logn),次数k,每节点时间复杂度O(klongn)
    • 一种记录已经遍历的候选集合方法
    • 发现其反转的奇偶特性,次数减半,同时将bannedSet的补集分成一半,每节点时间复杂度O(k/2 *long(n/2))
    • 而且由于记录了候选集合,遍历过可以防止重复访问,耗时会逐渐减小。
    • 最快的方法,发现候选集合是连续的。使用uf[x] = y;记录每个起始地址x,已经处理到地址y,也就是跳过了x~y的区域。

特征:问题能被分成多阶段求解(动态规划)

  • 抽象表示/题目特征(用于高效匹配题型)
  • 最优子结构
    • 问题的最优解所包含的子问题的解也是最优的
  • 无后效性
    • 即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
  • 子问题重叠
    • 如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。
  • 常见的解法流程
  • 强烈建议先写记忆化搜索的DFS,除非超时而且有固定个数(eg.500)可以写成固定大小的数组的递推.
    • 递推会比记忆化搜索快7~8倍左右
    • 递推的边界太不直观了,而且容易出错
  • 确定dp数组(dp table)以及下标的含义
    • 将原问题划分为若干 阶段,每个阶段对应若干个子问题,提取这些子问题的特征(称之为 状态);
  • 确定递推公式
    • 寻找每一个状态的可能 决策,或者说是各状态间的相互转移方式(用数学的语言描述就是 状态转移方程)。
  • dp数组如何初始化
  • 确定遍历顺序
  • 举例推导dp数组
    • 按顺序求解每一个阶段的问题。
  • 解法难点:
  • 记忆化搜索与从头递推是同时间复杂度的写法
  • 选择参数k,k的变化来分阶段, 寻找k与k+1间的转移关系
  • 二维DP,第二个维度间是怎么连续转移的?
  • 降低时间复杂度
    • 对于二维的DP,简单的阶段转移只需要O(1)时间,总时间O(mn)。
    • 复杂的题目阶段转移作为子问题,需要结合额外的方法简化时间复杂度
  • 降低空间复杂度
    • 分析每次转移使用和产生的数据,去除冗余的数据,二维DP从头递推使用滚动数组优化为O(n), 甚至可能简化为O(1)
  • 相关拓展考点/难度延伸
  • 相关例题以及核心考点
  • 如何选择分阶段参数和构建转移
    • 分阶段参数:输入序列的问题规模
    • 区间DP:
    • 中 300. 最长上升递增子序列
      • 分阶段参数:以A[i]为子序列结尾的最长子序列f(i)
      • 转移关系:遍历时新元素是否满足题意(递增),很简单加入序列,转移到更长的公共子序列
      • \[f(i)=\max[\max_{j \in [0,i-1]}f(j),0]+1, \textbf{if}\ nums[j]<nums[i]\]
      • 初始化f[0]=1
      • 子问题寻找是满足 nums[j]<nums[i]的最大的已有序列,通过维护辅助数据M[n]存储长度为i的末尾元素的最小值。
      • 贪心优化:上升子序列长 -> 上升慢 -> 新加元素尽可能小 -> 维护长度为 i的最长上升子序列的末尾元素的最小值
    • 最长上升子序列变种
      • 核心思想:以最后最后一个元素为参数来DP,才能比较是否上升。
      • 难 面试题 08.13. 堆箱子 往往 return *max_element(dp.begin(), dp.end());
    • 中 1027. 最长等差数列
      • 分阶段参数:和上一条相同,由于要判断递增和等差,需要两个元素信息。根据差值j情况又不同
      • 以nums[i]结尾的等差为j的最长子序列长度
      • 转移关系
      • 未优化版本O(n^3)\(\(f(i,j) = max[\max_{k\in [0,i-1]}f(k,j),0]+1, \textbf{if}\ a[i]-a[k]=j\)\)
      • 朴素的思路:把k维度优化掉,相同j的情况下,f随第一项单增,所以需要辅助数据记录值为a[i]-j对应的最后一个index
      • 思路二:优化差值j维度,从后向前遍历k,对于d=a[i]-a[k], 更新没处理的f(i,d)
    • 难 1187. 使数组严格递增
      • 分阶段参数:考虑包括index为i的前面元素,并且最后一个元素大小小于j的严格递增序列的最小操作数f(i,j)
      • 转移关系:被交换的\( a_2[p] \)也应该小于j
      • 初始想法:$$f(i,j)=min[min(f(i,k_1),\textbf{if} k_1\in [0,j-1]), $$$$f(i-1,a_1[i]),\textbf{if} a_1[i]<j, $$\(\(min(f(i-1,k_2)+1), \textbf{if}\ k_2\in \{a_2[0],\cdots,a_2[p]\} , a_2[p]<j )]\)\)
      • 初始化:f(-1,x)=0
      • 二维DP 有许多冗余信息:
        • 如果只有最后一个min,后面每行答案一样。加上第二个min,也能保存每行元素单调递减。所以第一行min是冗余的。
        • 由于每行是单调递减的,所以最后一个min应该取最后一个
      • 转移公式化简:$$f(i,j)=min[f(i-1,a_1[i]),\textbf{if} a_1[i]<j, $$$$ f(i-1,a_2[p])+1, \textbf{if} max p ,a_2[p]<j )]$$
      • 还存在两个问题:
        • 第二个维度过大\( 10^9 \):由于数的个数是有限的,可以排序后数位与数值一一对应,来处理数位。
        • 记忆化DFS时,考虑到第二个维度的范围比较大,可以用哈希表来记忆化。
        • 其次该转移公式容易DFS但是难以转化为递推?
      • 贪心优化:操作数少 -> 原本数组递增子序列越长 -> 被操作的数越紧密,剩下的未处理的数组的递增子序列就可能越长
    • 中 2606. 找到最大开销的子字符串
      • 子串的最大最小值
      • 分阶段参数:以A[i]为字串结尾的最大开销字串
      • 转移关系:新字串=前一个字串+新元素。保留每个以A[i]为字串结尾的字串数据来求最大最小值
    • 中 1043. 分隔数组以得到最大和
      • \[f(i)=\max_{j \in [max[i-k+1],i]}[f(j-1)+(i-j+1)\times \max_{p\in [j,i]}arr[p]]\]
      • 可以发现转移方程中有一个子区间最大值的子问题, 但是在写成递推时能顺便求解了。
    • 难 1335. 工作计划的最低难度
      • 求解a[n] d 个子数组的 最大值的和的最小值
      • 很自然的确定最后一个子数组的元素个数k来提取子问题,dfs(d-1, n-k)
      • 更快的,结合单调栈(太绕了,思路
    • 三维DP, 结果要从某个有限个数的维度遍历出来, 类似return [max(dfs(n,j) for j in range(J))
    • 中 6898. 字符串连接删减字母
      • 观察到有限的字母种类,
    • 0-1背包(受限的选择组合问题)
    • 分阶段参数:已经考虑的n个元素的个数,限制条件组成第二个参数
      • 也属于第一类:分阶段参数为输入序列的问题规模
    • OIWiki 有时限的最大化采药价值
      • 转移关系: 选与不选导致,\( max(f_{i-1}{k},f_{i-1}{k-w}+v) \)
      • 普通的0-1背包问题,使用滚动数组时,要注意遍历方向
    • 中 2597. 美丽子集的数目
      • 用乘法原理将总组合数问题分解成各个模数的组合数的乘积
      • 分阶段参数:包含的前n个元素的个数(单个模数的组合数求解的子问题里)
      • 转移关系:组合数关系(总组合数=选i的组合数+不选i的组合数)
    • 难 1125. 最小的必要团队
      • 状态压缩 + 集合版0-1背包
      • 状态压缩:由于输入数据一个小于16,一个小于64,压缩成二进制
      • 分阶段参数:dfs(i,j) 表示从前 i 个集合中选择一些集合,并集包含 j(题目限制需要),至少需要选择的集合个数。
      • 转移关系:dfs(i,j)=dfs(i−1,j delete set[i])+1
        • 边界:当j==0,限制已经满足了,dfs==0
      • 优化:递推查表法 vs 刷表法
    • 不相邻问题:打家劫舍四部曲
    • 一:一维序列不相邻问题,前一格和跳一格+自身里选最大
    • 二:一维环, 两种情况的最大值
      • 如果偷 nums[0],那么 nums[1]和 nums[n−1]不能偷,问题变成从 nums[2]到 nums[n−2]的非环形版本,调用前面的代码即可
      • 如果不偷 nums[0],那么问题变成从 nums[1]到 nums[n−1]的非环形版本
    • 三:树上的不相邻问题,很像树形DP
      • 需要一个根,来确定子树(DFS)方向。除开当前遍历的节点,还需要一个变量表示是否选择
      • 转移关系如下:x为i的子节点
        • 未选\( f(i,0)= \sum{max(f(x,1),f(x,0))} \)
        • 已选\( f(i,1)= \sum f(x,0) + a_i \)
    • 三拓展 难 2646. 最小化旅行的价格总和
      • 选择的点是1/2, 未选择是原代价。 而且每个节点有次数\( k_i \)
      • 转移关系如下:x为i的子节点
        • 未选\( f(i,0)= \sum{min(f(x,1),f(x,0))} + a_i * k_i \)
        • 已选\( f(i,1)= \sum f(x,0) + 0.5 a_i k_i \)
    • 四:和二分答案结合
  • 组合数问题:
    • 计数DP: 中 1079. 活字印刷
    • f[i][j] 表示用前 i 种字符构造长为 j 的序列的方案数。选k个字符进行放置。
  • 降低时间复杂度
  • 与二进制结合

特征: 数量有限的有无/是非关系(状态压缩)

  • 抽象表示/题目特征(用于高效匹配题型)
  • 题目或其子问题有明显的0-1关系,考虑位运算简化判断
  • 常见的解法流程
  • 将状态用二进制表示
    • 位运算按位与(&)、按位或(| )、按位异或(^)、取反(~)、左移(<<)、右移(>>)
  • 位运算替代状态转换
    • 将元素 x 变成集合 {x},即 1 << x
    • 判断元素 x 是否在集合 A 中,即 ((A >> x) & 1) == 1
    • 计算两个集合 A,B 的并集 \( A\cup B \),即 A | B
    • 例如 110 | 11 = 111
    • 计算 \( A \setminus B \),表示从集合 A 中去掉在集合 B 中的元素,即 A & ~B
    • 例如 110 & ~11 = 100
    • 全集 U={0,1,⋯ ,n−1},即 (1 << n) - 1
  • 解法难点:
  • 相关拓展考点/难度延伸
  • 相关例题以及核心考点
  • 中 1042. 不邻接植花
  • 难 1125. 最小的必要团队
  • 难 1483. 树节点的第 K 个祖先
    • 去掉 k 的最低位的 1: k &= k - 1
    • 转化为二进制的DP递推

问题:求动态区间(子区间)内的最大最小值

  • 抽象表示/题目特征(用于高效匹配题型)
  • 如问题
  • 常见的解法流程
  • 构建最小单调栈,区间变化的时候push,并且pop大于新元素的元素
  • 有时候也是动态规划, 比如 难 1335. 工作计划的最低难度
  • 解法难点:
  • 额外的要求导致的额外的判读和信息存储
  • 相关拓展考点/难度延伸
  • 相关例题以及核心考点
  • 中难 907. 子数组的最小值之和
    • 左右单调栈 + 贡献法
  • 难 2617. 网格图中最少访问的格子数
    • 解法:反转DP方向 + 二分查找单调栈
    • 求min,每一步在push时构建底大顶小递增的单调栈,后push元素要是能使用优先级最高的最小值,按题意新元素最近(idnex相差1), 所以能丢弃前面结果更大的元素。
    • 注意:由于是反转DP,由于新元素位于栈末尾,而且新元素的index小,所以单调栈中index也是单调递减的,所以能二分查找。
  • 难 1335. 工作计划的最低难度
    • 最快的方法: 单调栈递推DP

问题:求动态区间(子区间)内的最大值

  • 只能用动态规划(递推)
  • 问:子区间最大和,一维DP,对象是区间的末尾元素
  • 进阶(RedStar 230724):子区间能改变一个值为x,问最大和。二维DP,第二维度是是否在区间内改变,再根据改变的位置是不是末尾来分成两种情况 dp[j][1]=max[max(dp[j-1][0]+x,x),max(dp[j-1][1]+nums[j],nums[j])]

问题:求动态区间(子区间)内的出现元素次数

  • 相关例题以及核心考点
  • 难 1157. 子数组中绝对众数
    • 绝对众数的快速暴力解法见文末:区间长度n时,每次查询需要O(n)
    • 分组:
    • 区间长度 \( \leq s \),直接暴力即可。
    • 区间长度 \( > s \),由于绝对众数定义其出现次数 \( >\frac{s}{2} \) ,因此可能的答案个数 \( \leq\frac{2n}{s} \)
    • \( s=\sqrt{2n} \) 时两者情况时间复杂度相等。
    • 线段树
    • 由于摩尔投票具有区间可加性,使用线段树可以直接查询区间内的绝对众数。然后查找众数在区间内的次数(map保存每个数的出现index有序集合,然后使用upper_bound和lower_bound)。 C++代码

问题:区间数值求和、绝对值、求个数(有限种类、奇偶个数)

特征:所有子数组x值的和 mod 10^7

问题:求满足条件的参数的最小/最大值

  • 抽象表示/题目特征(用于高效匹配题型)
  • 题目要求
    • 求解满足条件的一个最大或者最小值、最小最大值或者最大最小值
    • 单调性,答案是个临界值,两侧分别是满足条件和不满足条件。
  • 如果猜测一个答案mid,能作为条件参与到满足题目条件的判断中。
  • 答案数值有明显的上界和下界,作为二分答案的leftright
  • 常见的解法流程
  • 二分答案mid,判断当前mid的数值是否满足题目条件,来寻找满足条件的最大/最小答案。
  • 解法难点:
  • 每次判断是否满足题目如何实现
  • 相关拓展考点/难度延伸
  • 相关例题以及核心考点
  • 题单
  • 2560. 打家劫舍 IV
    • 选择k个中最大数值的最小值,将最大的最小值转换成不超过mid的限制
    • => 最大数值不超过mid的最大个数p的求解, p < k 则不满足条件
    • 条件判断转换成DP
  • 2616. 最小化数对的最大差值
    • 选择p数对的最大差值的最小值
    • => 最大差值不超过mid的最多数对数k, k < p则不满足条件
    • 条件判断转换成DP
  • 难 2528. 最大化城市的最小供电站数目
    • 部署电站k个后,城市最小电量的最大值
    • => 使得城市最小电量不小于mid,最少需要p个额外电站部署, p > k 则不满足条件
  • 华为 2023.04.12-实习笔试-第一题-购物系统的降级策略
  • 华为 P1006. 2022.9.21-数组取min
  • 华为230426 机试三
    • 二分答案,每种情况是一个最强祝福力场子问题

常见数学相关基础

模运算相关基础

  • 同余定理:
  • 给定一个正整数m,如果两个整数a和b满足a-b能够被m整除,即(a-b)/m得到一个整数,那么就称整数a与b对模m同余,记作a≡b(mod m)。对模m同余是整数的一个等价关系。
  • 性质
  • 对称性:若a≡b(mod m),则b≡a (mod m);
  • 传递性:若a≡b(mod m),b≡c(mod m),则a≡c(mod m);
  • 同余式相加:若a≡b(mod m),c≡d(mod m),则a+c≡b+d(mod m);
  • 同余式相乘:若a≡b(mod m),c≡d(mod m),则ac≡bd(mod m)。
  • 费马小定理
  • 若 p 为素数,gcd(a, p) = 1,则 \(\(a^{p - 1} \equiv 1 \pmod{p}\)\)
  • 另一个形式:对于任意整数 a,有 \(\(a^p \equiv a \pmod{p}\)\)
  • 欧拉定理
  • 欧拉函数:\( \varphi(n) \),表示的是小于等于 n 和 n 互质的数的个数。
    • \( \varphi(1) \) = 1。
    • 当 n 是质数的时候,有 \( \varphi(n) \) = n - 1。
  • 若 gcd(a, m) = 1,则 \(\(a^{\varphi(m)} \equiv 1 \pmod{m}\)\)

模运算,模数

质数(素数)

  • 质数(素数)相反的概念是合数
  • 平凡约数:1和它本身称为平凡约数;大于1小于它本身的约数称为非平凡约数。
  • 高效判断是否为质数,重点在上界i * i <= n
  • 如果某个整数大于 1 ,且不存在除 1 和自身之外的正整数因子,则认为该整数是一个质数。
bool is_prime(int n) {
  for (int i = 2; i * i <= n; ++i)
    if (n % i == 0)
      return false;
  return n >= 2; // 1 不是质数
}
  • 最快速的预处理(埃氏筛):MAX内的质数序列, 考虑从头deny候选元素
  • 由于没有分支与访问有规律,会比复杂度低的欧拉筛快

存储质数和非质数的版本:

const int MX = 1e5;
bool np[MX + 1]; // 质数=false 非质数=true
int init = []() {
    np[1] = true; // 1 不是质数
    for (int i = 2; i * i <= MX; i++) {
        if (!np[i]) {
            for (int j = i * i; j <= MX; j += i) {
                np[j] = true;
            }
        }
    }
    return 0;
}();

质数push2vector:

const int MX = 1e6;
vector<int> primes; // 建议一个vector,一个数组
bool np[MX + 1]; // 默认是0

int init = []() {
    for (int i = 2; i <= MX; i++) { // 不能是 i*i
        if (!np[i]) {
            primes.push_back(i); // 预处理质数列表, 写在里面,不能是 i*i
            // for (int j = i; j <= MX / i; j++) // 避免int溢出的写法
            //     np[i * j] = true; //deny后续元素, i*i 开始
            // or
            for(int j = i*i; j <= MX; j+=i){
                np[j] = true;
            }
        }
    }
    return 0;
}();
  • 埃氏筛:通俗的写法
    vector<int> generatePrimes(int n) {
      vector<bool> isPrime(n + 1, true);
      vector<int> primes;
    
      for (int p = 2; p * p <= n; ++p) {  // 使用了 i*i,素数要额外push
          if (isPrime[p]) {
              // primes.push_back(p);
              for (int i = p * p; i <= n; i += p) { // 新排除的是新质数的倍数
                  isPrime[i] = false;
              }
          }
      }
    
      for (int p = 2; p <= n; ++p) {
          if (isPrime[p]) {
              primes.push_back(p);
          }
      }
      return primes;
    }
    

基于费马小定理的O(log n)的伪素数判断方法。但是机试一般不会考。

众数

  • 绝对众数求解,
  • 绝对众数:该众数出现的次数大于N/2。
  • 摩尔投票算法:寻找一组元素中占多数元素(不能刚好占一半)的常数空间级时间复杂度算法。
  • 每次从序列里选择两个不相同的数字删除掉(或称为“抵消/对拼消耗/争擂台”),最后剩下一个数字或几个相同的数字,就是出现次数大于总数一半的那个。
j=k=0;
for(i=left;i<=right;i++)
  if(a[i]==j)
    k++;
  else if(k)
    k--;
  else{
    j=a[i];
    k=1;
  }

gcd, lcm

  • 最大公约数gcd,最小公倍数lcm
  • gcd(a, b) = gcd(b, a % b) 注意是求余数
    • 虽然平时都是调用函数,但是常见实现是
    • 欧几里得算法(辗转相除法): 原理: gcd(a,b)=gcd(b,a mod b), 大数除以小数得到余数,对三个数中的两小数重复,直到出现整除,较小数为最大公约数。
    • 更相减损术: 原理: gcd(a,b)=gcd(b,a - b), 大数减小数得到C,对三个数中的两小数重复,直到出现相等,即为最大公约数。
  • 最小公倍数lcm可以由gcd推导:lcm(a, b) = a * b / gcd(a, b)
  • 多个数的最大公约数: 由于有传递性,可以将两个树的gcd,传递下去
  • 多个数的最小公倍数ans = lcm(a, b, c) = lcm(lcm(a, b), c),或者ans = tmp/gcd(tmp,c)*c先除后乘防止溢出。其中tmp=lcm(a,b)或者递归。

位运算

  • 中 2411. 按位或最大的最小子数组长度
  • 每个元素的影响等于并集,按位或最大==数字1最多,即最大的并集
    • 没有增大==或结果相同=>子集=>前面不需要再遍历,剪枝
  • 中 1072. 按列翻转得到最大值等行数
  • 逆向思考,相等行间的关系
    • 相同的行1111 1111 or 0000 0000. 反转列变成 1101 1101 and 0010 0010
    • 可以发现将第一位变成1 后,答案就是同为某序列 1101的最大个数
  • 注意异或1相当于取反,异或0相当于不变。
  • 异或 也可以视作「不进位加法(减法)」或者「模 222 剩余系中的加法(减法)」

组合数问题

  • 基本公式:
  • 常见分解技巧:
  • 组合数关系(总组合数=选i的组合数+不选i的组合数):
  • 乘法原理将总组合数分解成各个部分的组合数的乘积
  • 组合问题,常见解法:
  • 加加减减回溯法
  • 动态规划

不进位的加法

进制转换

  • n进制转换为十进制: 简单,a[i] * n^i
  • 十进制转换为n进制(n为正数)
  • 除n取余数,最后结果取反(reverse)
  • 十进制转换为(-n)进制(n为正数)
  • 每次取的余数k保证为正数在0到n-1之间。T=(-n)*p+k
  • 否则,调整T=(-n)*(p+1)+(k+n)。余数+n变正数,商++
  • 中 1073. 负二进制数相加
  • 但是这道题不能用进制转换做,会数据会溢出。 只能模拟按位加法,
  • 实现时有一个小问题, 关于负数整除与负数取余
    vector<int> int2array(long long num){
      const int k = -2;
      vector<int> result;
      while(num){
          if(num > 0 || num%k==0){
              // num%k 余数 大于等于0
              result.push_back(num%k);
              num /= k;
          }else{
              result.push_back(num%k-k);
              num = num/k + 1;
          }
      }
      reverse(result.begin(), result.end());
      if(result.size()==0){return {0};}
      return result;
    }
    

负数整除与负数取余

  • 负数整除
  • C++ 除法采用了向零取整(截断取整):向0方向取最接近精确值的整数,换言之就是舍去小数部分。
  • 所以有(-a)/b==-(a/b)
  • 负数取余
  • 结论a%b=cc的符号与a相同
  • 证明: 由于 被除数÷除数=商……余数,所以余数=被除数-商×除数。其中除数可由上面的C++ 除法推导
    • 讨论 7%(-4) (-7)%4 (-7)%(-4) 三种情况可知

模板写法

  • 抽象表示/题目特征(用于高效匹配题型)
  • 常见的解法流程
  • 解法难点:
  • 框架复杂?
  • 相关拓展考点/难度延伸
  • 相关例题以及核心考点
  • 1143 HARD xxx :考察xxx

需要进一步的研究学习

暂无

遇到的问题

做了30天的leetcode,发现不会的还是不会。我就知道我

学而不思则罔

脑中一团浆糊,虽然学习了一些常见题型的框架、解法以及例题。

但是遇到新题目,是否能使用这些方法,以及如何转换使用还是没考虑清楚。

开题缘由、总结、反思、吐槽~~

参考文献

https://oi-wiki.org/

许多思路来自:灵茶山艾府

Graph Algorithms: Pagerank

Pagerank

  1. Network and social network can be identified as a weighted graph
  2. How to do the important ranking is Pagerank

how to design a graph when pr executing memory access jump data-array very random?

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

上面回答部分来自ChatGPT-3.5,没有进行正确性的交叉校验。

https://zhuanlan.zhihu.com/p/137561088

Benchmark : GUPS

Introduction

GUPS (Giga十亿 UPdates per Second) is a measurement that profiles the memory architecture of a system and is a measure of performance similar to MFLOPS.

The HPCS HPCchallenge RandomAccess benchmark is intended to exercise the GUPS capability of a system, much like the LINPACK benchmark is intended to exercise the MFLOPS capability of a computer. In each case, we would expect these benchmarks to achieve close to the "peak" capability of the memory system. The extent of the similarities between RandomAccess and LINPACK are limited to both benchmarks attempting to calculate a peak system capability.

definition of GUPS

GUPS is calculated by identifying the number of memory locations that can be randomly updated in one second, divided by 1 billion (1e9).

  • The term "randomly" means that there is little relationship between one address to be updated and the next, except that they occur in the space of one half the total system memory. (只用一半内存?)
  • An update is a read-modify-write operation on a table of 64-bit words. An address is generated, the value at that address read from memory, modified by an integer operation (add, and, or, xor) with a literal value, and that new value is written back to memory.

Extensibility

  • We are interested in knowing the GUPS performance of both entire systems and system subcomponents --- e.g., the GUPS rating of a distributed memory multiprocessor the GUPS rating of an SMP node, and the GUPS rating of a single processor.
  • While there is typically a scaling of FLOPS with processor count, a similar phenomenon may not always occur for GUPS.

Principle

Select the memory size to be the power of two such that 2^n <= 1/2 of the total memory. Each CPU operates on its own address stream, and the single table may be distributed among nodes. The distribution of memory to nodes is left to the implementer. A uniform data distribution may help balance the workload, while non-uniform data distributions may simplify the calculations that identify processor location by eliminating the requirement for integer divides. A small (less than 1%) percentage of missed updates are permitted.

Installation

Download

official web or from GitHub

Usage

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

上面回答部分来自ChatGPT-3.5,没有进行正确性的交叉校验。

Dynamic Programming

动态规划简介

将一件事情分成若干阶段,然后通过阶段之间的转移达到目标。由于转移的方向通常是多个,因此这个时候就需要决策选择具体哪一个转移方向。

动态规划所要解决的事情通常是完成一个具体的目标,而这个目标往往是最优解。并且:

  1. 阶段之间可以进行转移,这叫做动态。
  2. 达到一个可行解(目标阶段) 需要不断地转移,那如何转移才能达到最优解?这叫规划。

每个阶段抽象为状态(用圆圈来表示),状态之间可能会发生转化(用箭头表示)。可以画出类似如下的图:

性质:

  1. 最优子结构
  2. 问题的最优解所包含的子问题的解也是最优的
  3. 注意: 具有最优子结构也可能是适合用贪心的方法求解。
  4. 无后效性
  5. 已经求解的子问题,不会再受到后续(父问题)决策的影响。
  6. 即子问题的解一旦确定,就不再改变,不受在这之后、包含它的问题的求解决策的影响。
  7. 子问题重叠
  8. 如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。

基本思路

对于一个能用动态规划解决的问题,一般采用如下思路解决:

  • 将原问题划分为若干 阶段,每个阶段对应若干个子问题,提取这些子问题的特征(称之为 状态);
  • 寻找每一个状态的可能 决策,或者说是各状态间的相互转移方式(用数学的语言描述就是 状态转移方程)。
  • 按顺序求解每一个阶段的问题。 如果用图论的思想理解,我们建立一个 有向无环图,每个状态对应图上一个节点,决策对应节点间的连边。

难点,重点

如何找到转移关系:

  1. 可以伸缩的维度: 一般是数组的下标,或者是字符串的下标,或者是树的节点。也可以是问题的query数值。
  2. 分情况讨论的,最大值为不同情况的最大值。方案数为所有情况的总和。

DP分类

  • 背包DP
  • 0-1背包
    • 定义:每个物体只有两种可能的状态(取与不取),对应二进制中的 0 和 1,这类问题便被称为「0-1 背包问题」
    • 一般有个总cost为W来限制选择
    • 基础版:可以通过滚动数组减少空间占用,但是注意遍历方向
    • 常见变形
    • 至多装capacity,求方案数/最大价值和
    • 恰好装capacity,求方案数/最大/最小价值和
      • 方案数,将转移公式最大值,改成加法。注意会多一维度数据。基础1+提升2
    • 至少装capacity,求方案数/最小价值和
    • 变种:总cost等于W时,和为half的子集是否存在
    • 方法一:
      • 背包的体积为sum / 2
      • 每个物品 cost = value
      • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
    • 方法二:
      • 状态:前i个元素,和为j的子集是否存在
      • 转移关系: \( f(i,j)=(f(i-1,j)+f(i-1,j-a[i])>0)?1:0 \) 注意: f[i,0]=1
    • 变种:选择和为k的子集的
    • 变种2:总cost等于W时,和为half的子集的个数 题目:2022.9.23-求和
    • 状态:前i个元素,和为j的子集个数
    • 转移关系: \( f(i,j)=f(i-1,j)+f(i-1,j-a[i]) \) 注意: f[i,0]=1
  • 完全背包: 每个物体可以选择多次
    • 转移关系: 也可以由f(i, j-w)转移到f(i,j)
  • 多重背包: 每个物体可以选择\( k_i \)次

    • 朴素思路: 每步可以总\( k_i \)里选最大, \( f_{i,j} = max_{k=0}^{k_i}(f_{i-1,j - k \times w_{i}} + v_{i} \times k ) \)
    • 二进制分组优化
    • 思想:多重背包,其实是有很多重复元素的0-1背包,通过二进制来唯一表示选取,来最小0-1背包的可选物品数
    • 具体操作: 方法是:将第 i 种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为\( 1,2,4,\ldots,2{k-1},n-2k+1 \),且 k 是满足\( n[i]-2^k+1>0 \)的最大整数。例如,如果 n[i] 为 13,就将这种物品分成系数分别为 1, 2, 4, 6 的 4 件物品。
    • 效果:将第 i 种物品分成了 O(log⁡n[i]) 种物品,将原问题\( O(V\times\sum n[i]) \)转化为了复杂度为\( O(V\times\sum \log n[i]) \)的 01 背包问题
      int num[maxn][2], dp[maxn];
      int N, V, c, w, n, tot;
      memset(dp, 0, sizeof dp);
      cin >> V >> N; tot = 1;
      for(int i = 1; i <= N; ++i)
      {
        cin >> c >> w >> n; // 第 i 物品的费用、价值和数量
        for(int k = 1; k < n; k<<=1)//左移求下一个所需二进制数 
        {
          num[tot][0] = k*c; // 添加 子物品
          num[tot++][1] = k*w;
          n -= k;
        }
        num[tot][0] = n*c; // 添加n[i]-2^k+1的 子物品
        num[tot++][1] = n*w;
      }
      for(int i = 1; i < tot; ++i)
      {
        for(int j = V; j >= num[i][0]; --j) // 0-1 背包 滚动数组实现
          dp[j] = max(dp[j], dp[j-num[i][0]]+num[i][1]);
      }
      
    • 单调队列优化 to finish
  • 区间DP

  • \( f(i,j)=max_k{f(i,k)+f(k+1,j)+cost} \)
  • DAG 上的DP
  • DAG(Directed Acyclic Graph) 即 有向无环图,一些实际问题中的二元关系都可使用 DAG 来建模,从而将这些问题转化为 DAG 上的最长(短)路问题。
  • to do
  • 树形 DP 往往需要递归DFS
  • 分阶段参数:当前子树的根节点
  • 转移关系:当前子树的根节点和其子节点的关系
  • 例题: to do

记忆化搜索

  • 子问题间重叠的部分会有很多,同一个子问题可能会被重复访问多次,效率还是不高。
  • 思路: 记忆化,参数一定,那么返回值也一定不会变,因此下次如果碰到相同的参数,我们就可以将上次计算过的值直接返回,而不必重新计算。这样节省的时间就等价于去除的重叠子问题的耗时。
  • 解决方法:把每个子问题的解存储下来,通过记忆化的方式,确保每个子问题只被计算一次。

如何写记忆化搜索

  • 方法一
  • 把这道题的 dp 状态和方程写出来
  • 根据它们写出 dfs 函数
  • 添加记忆化数组
  • 方法二
  • 写出这道题的暴搜程序(最好是 dfs)
  • 将这个 dfs 改成「无需外部变量」的 dfs
  • 添加记忆化数组

实例:朴素DFS递归,实现记忆化

爬楼梯问题的暴力普通递归DFS代码

function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  return climbStairs(n - 1) + climbStairs(n - 2);
}
添加与DFS参数相关的记忆化数组,将这个 dfs 改成「无需外部变量」的 dfs。
memo = {}
def climbStairs(n):
  if n == 1:return 1
  if n == 2: return 2
  if n in memo: return memo[n]
  ans = func(n - 1) + func(n-2)
  memo[n] = ans
  return ans
climbStairs(10)

优化技巧

状态压缩 & 动态规划

状压+动态规划(DP, Dynamic Programming)

使用的数有限(共 10 个),并且使用到的数最多出现一次,容易想到使用「状压 DP」来求解:我们使用一个二进制数来表示具体的子集具体方案。

定义 f[state] 为当前子集选择数的状态为 state 时的方案数,state 为一个长度 10 的二进制数,若 state 中的第 k 位二进制表示为 1,代表数值 p[k] 在好子集中出现过;若第 k 位二进制表示为 0 代表 p[k] 在好子集中没出现过。

状态压缩

状态压缩有关,比如用 4 个字节存储状态

需要进一步的研究学习

动态规划 与 np完全的关系

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

视频: https://www.bilibili.com/video/BV1Xj411K7oF/?vd_source=5bbdecb1838c6f684e0b823d0d4f6db3

https://oi-wiki.org/dp/basic/

https://www.zhihu.com/question/316416327/answer/626807333

Algorithm: leetcode

渐进符号

排序算法

* 排序算法的稳定性:排序前后相同元素的相对位置不变,则称排序算法是稳定的;否则排序算法是不稳定的。 * 计数排序 中 k是数据出现的范围 * 基数排序时间复杂度为O(N*M),其中N为数据个数,M为数据位数。

按照实现方法分类

  • 选择排序
  • 直接选择排序:N轮,每轮变小的范围内找到最小值,然后与第i个交换。
  • 堆排序:
    • 最大堆与数组的映射关系 大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
    • 维护每轮范围变小的堆,每次将最大的堆顶移动到最后。每次维护堆需要O(logn)交换,把pop上来的小元素沉底到叶节点。
    • 初始建堆需要O(nlogn)
  • 插入排序
  • 直接插入:N轮,每轮从i开始向前插入(移动来交换),直到插入到合适的位置
  • 希尔排序: 希尔排序是插入排序改良的算法,
    • 希尔排序步长从大到小调整,第一次循环后面元素逐个和前面元素按间隔步长进行比较并交换,
    • 直至步长为1,步长选择是关键。
  • 交换排序
  • 冒泡排序:冒泡N轮,每轮变小的范围内确定最后一个。
  • 快速排序:在数组中随机选一个数(默认数组首个/末尾元素),数组中小于等于此数的放在左边部分(交换到前面的排列),大于此数的放在右边部分,这个操作确保了这个数是处于正确位置的,再对左边部分数组和右边部分数组递归调用快速排序,重复这个过程。

  • 分治合并

  • 归并排序: 首先让数组中的每一个数单独成为长度为1的区间,然后两两一组有序合并,得到长度为2的有序区间,依次合并进行得到长度为4、8、16的区间,直到合成整个区间。

  • 计数排序:数据出现的范围k << O(n)时,或者k=O(n)都可以采用该方法。

  • 基数排序:对数据的每一位(共M位)从低位到高位进行stableSort。大部分时候选择计数排序O(N+k)。总时间复杂度O(M*(N+k))
  • 桶排序:类似计数排序的思想,但是一般是对于区间等分为桶。桶内可以采用插入排序。n个元素n个桶,数学期望是O(n)

堆排序代码细节

void heapSort(int array[], int n)
{
    int i;
    //先建立堆
    for (i=n/2;i>0;i--)
    {
        HeapAdjust(array,i,n);//从下向上,从右向左调整
    }
    //交换最大堆顶,重复n次
    for( i=n;i>1;i--)
    {
        swap(array, 1, i);
        HeapAdjust(array, 1, i-1);//从上到下,从左向右调整
    }
}
void HeapAdjust(int array[], int s, int n )
{
    int i,temp;
    temp = array[s];
    for(i=2*s;i<=n;i*=2)
    {
        if(i<n&&array[i]<array[i+1])
        {
            //交换左右子树最大的那个
            i++;
        }
        if(temp>=array[i])
        {
            //找到了插入的合适的位置,子节点更小,父节点更大
            break;
        }
        // 将节点向上移动
        array[s]=array[i];
        s=i;
    }
    //将最顶部插入到合适的位置
    array[s]=temp;
}
void swap(int array[], int i, int j)
{
    int temp;
    temp=array[i];
    array[i]=array[j];
    array[j]=temp;
}

排序相关的问题

  • 既然时间复杂度堆排序、归并排序好于快排,为什么C++的qsort使用的是快排
  • 快速排序访存更友好,堆排序访问是跳跃的
  • 对于同样的数据,排序过程中,堆排序算法的数据交换次数多于快排
    • 堆排序建立堆,与堆顶的交换,很多时候都是无用功
  • 在数据量小的时候快速排序当属第一,堆排序最差,但随着数据的不断增大归并排序的性能会逐步赶上并超过快速排序,性能成为三种算法之首。
  • C++ 的 stable_sort 是基于归并排序的

LeetCode 常见算法

拓扑排序

拓扑排序常用来确定一个依赖关系集(图关系)中,事物发生的顺序。

带信号量判断的无依赖队列来实现,入队无依赖集合,出队的无依赖元素(add to result)去除后续元素的依赖信号量,信号量为0代表无依赖,可以入队。

无环图(树图)中最长距离

找到图中距离最远的两个节点与它们之间的路径:

以任意节点 pp 出现,利用广度优先搜索或者深度优先搜索找到以 pp 为起点的最长路径的终点 xx;

以节点 xx 出发,找到以 xx 为起点的最长路径的终点 yy;

xx 到 yy 之间的路径即为图中的最长路径,找到路径的中间节点即为根节点。

树状数组

https://leetcode-cn.com/circle/article/9ixykn/

https://leetcode-cn.com/problems/range-sum-query-mutable/

广度搜索确定图中各点对0点最近距离

//input [[0,1],[1,2]]
//维护
vector<vector<int>> adj(n); //先找出每个点的有关边
vector<bool> visit(n, false);   //维护已访问元素

queue<int> qu;

qu.emplace(0);
visit[0] = true;
int dist = 1;

while (!qu.empty()) {
    int sz = qu.size();
    for (int i = 0; i < sz; ++i) {
        int curr = qu.front();
        qu.pop();
        for (auto & v : adj[curr]) {
            if (visit[v]) {
                continue;
            }
            qu.emplace(v);
            //对应处理
            visit[v] = true;
        }
    }
    dist++;
}

2进制数表示子集合

对集合大小为n,可以用大于等于0小于1<<n2^n-1个数字来表示子集。

但是对每个子集都会单独计算,有重复。 不如用按每位是否存在回溯

#2044
class Solution {
public:
    int ans = 0;
    int countMaxOrSubsets(vector<int>& nums) {
        int maxOr = 0;
        for (auto n: nums){
            maxOr = n | maxOr;
        }
        dfs(nums, maxOr, 0 , 0);
        return ans;
    }
    void dfs(vector<int>& nums, int maxOr, int idx, int cur){
        if (cur == maxOr){
            ans += 1 << (nums.size()-idx);
            return;
        }

        if (idx == nums.size()){
            return;
        }

        dfs(nums, maxOr, idx+1, cur | nums[idx]);
        dfs(nums, maxOr, idx+1, cur);

    }
};

2进制表示使用状态true false

int 可以表示32个元素的使用情况

https://leetcode.cn/problems/can-i-win/

前缀和和差分

前缀和差分 是一组互逆的方法;他们的关系和积分求导 实质是一样的。前缀和可以帮我们通过预处理快速的求出区间的和;差分则可以快速帮助我们记录区间的修改。

将区间前一个加一,最后一个减一实现。

leetcode 798

预处理查询的数组

通过预处理记录信息来减少查询的时间

leetcode 2055

二分法

二分寻找满足条件的最小整数, 注意left + 1 < rights >= cars

while (left + 1 < right) { // 开区间
    long long mid = (left + right) / 2, s = 0;
    for (int r : ranks)
        s += sqrt(mid / r);
    (s >= cars ? right : left) = mid;
}
// 作者:灵茶山艾府
// 链接:https://leetcode.cn/problems/minimum-time-to-repair-cars/solutions/2177199/er-fen-da-an-pythonjavacgo-by-endlessche-keqf/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

直接模拟

最常用的方法

哈希算法

根据设定的哈希函数H(key)和处理冲突方法将一组关键字映象到一个有限的地址区间上的算法。也称为散列算法、杂凑算法。

哈希冲突

一般有:开放定址法、链地址法(拉链法)、再哈希法、建立公共溢出区

LeetCode 代码优化加速

cin.tie与sync_with_stdio加速输入输出

std::ios::sync_with_stdio(); 是一个“是否兼容stdio”的开关,C++为了兼容C,保证程序在使用了std::printf和std::cout的时候不发生混乱,将输出流绑到了一起。也就是 C++标准streams(cin,cout,cerr…) 与相应的C标准程序库文件(stdin,stdout,stderr)同步,使用相同的 stream 缓冲区。 默认是同步的,但由于同步会带来某些不必要的负担,因此该函数作用是使得用户可以自行取消同步。

cin.tie(NULL) 取消 cin 和 cout 的绑定。

这对于输入数据个数在10^5以上的程序十分有效

static int x = []() {
    std::ios::sync_with_stdio(false);
    cin.tie(NULL);
    return 0;
}();
// or
static const auto io_sync_off = []() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    return nullptr;
}();

or

int init = []() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    return 0;
}();

问题拆分循环调用

不如从底层动态规划合并,不要嵌套函数调用,还可以用二维数据,数据局部性较好。

https://leetcode-cn.com/problems/optimal-division/submissions/

不一定要执着数据只遍历一遍

可以将复杂的一次遍历,拆开成两次遍历,一次处理数据并存储,一次遍历统计。速度反而会快

简单递归循环

用while代替函数递归调用,eg二分法

减少if语句

可以保存分支的值来实现(1748)

//0ms
int sumOfUnique(vector<int>& nums) {
 int state[101]{}, ans = 0, d[101]{1,-1};
 for(int x: nums) ans += d[state[x]++] * x;
 return ans;
}

//4ms
int sumOfUnique(vector<int>& nums) {
 array<char,101> isshowed {};
 int sum=0;
 for(auto& num:nums){
  if(isshowed[num]==0){
   isshowed[num]=1;
   sum+=num;
  }else if(isshowed[num]==1){
   isshowed[num]=2;
   sum-=num;
  }
 }
 return sum;
}

通过判断筛选掉

无用的遍历计算(1219)

减少for循环

循环展开,只有一两种情况就不要写for循环了

注意的细节

计算防止溢出

int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2

转化成加减,而不用乘法

A < B/2
变成
A < B-A

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

https://oi-wiki.org/dp/basic/

https://github.com/azl397985856/leetcode/blob/master/thinkings/dynamic-programming.md

作者:AC_OIer 链接:https://leetcode-cn.com/problems/the-number-of-good-subsets/solution/gong-shui-san-xie-zhuang-ya-dp-yun-yong-gz4w5/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Hash map

hash的相关基本知识

https://www.cnblogs.com/guoben/p/13339280.html

散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度

α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。

unordered_map定义

插入元素时,根据待插入元素的关键码,以hashfunc计算出该元素的存储位置,在结构中按此位置进行存放

搜索元素时,对关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置去元素比较,若关键码相等,则搜索成功

C++11 定义了std::hash

template<
    class Key,
    class T,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator< std::pair<const Key, T> >
> class unordered_map;
很明显可以reserve设置大小

unordered_map详解

http://c.biancheng.net/view/7231.html

自定义结构体hash函数

hashfunc设计原则

哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许m个地址时,其值域必须在0-(m-1)之间

哈希函数计算出来的地址能均匀分布在整个空间中

哈希函数应该比较简单

举例

https://blog.csdn.net/HappyKocola/article/details/74188452

如果想让自定义的class作为key(unordered_map)来使用unordered_map,需要实现:

  1. 哈希函数,需要实现一个class重载operator(),将自定义class变量映射到一个size_t类型的数。一般常用std::hash模板来实现。
  2. For two parameters k1 and k2 that are equal, std::hash<Key>()(k1) == std::hash<Key>()(k2).
  3. For two different parameters k1 and k2 that are not equal, the probability that std::hash<Key>()(k1) == std::hash<Key>()(k2) should be very small, approaching 1.0/std::numeric_limits<std::size_t>::max().
  4. 判断两个自定义class类型的key变量是否相等的函数,一般在自定义class里重载operator==。

 template <>
    struct hash<Myclass>
    {
        size_t operator()(const Myclass &k) const
        {
            int h = k.first;
            for (auto x : k.second)
            {
                h ^= x;
            }
            return h;
        }
    };
或者
struct MyHash
{
  size_t operator() (const pair<int,int>& p) const
  {
    return hash<long long>()( (static_cast<long long>(x.first) ) 
    ^ ( (static_cast<long long>(x.first))<<32) );
  }
};

防止冲突的自定义hash

防止黑客攻击

https://blog.csdn.net/qq_21433411/article/details/88365164

struct custom_hash {
    static uint64_t splitmix64(uint64_t x) {
        // http://xorshift.di.unimi.it/splitmix64.c
        x += 0x9e3779b97f4a7c15;
        x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
        x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
        return x ^ (x >> 31);
    }

    size_t operator()(uint64_t x) const {
        static const uint64_t FIXED_RANDOM = chrono::steady_clock::now().time_since_epoch().count();
        return splitmix64(x + FIXED_RANDOM);
    }
};

结构体万能hash

解释见转载的作者

template <typename T>
inline void hash_combine(size_t& seed, const T& val)
{
    seed ^= std::hash<T>()(val) + 0X9E3779B9 + (seed << 6) + (seed >> 2);
}

template <typename T>
inline void hash_val(size_t& seed, const T& val)
{
    hash_combine(seed, val);
}

template <typename T, typename... Types>
inline void hash_val(size_t& seed, const T& val, const Types&... args)
{
    hash_combine(seed, val);
    hash_val(seed, args...);
}

template <typename... Types>
inline size_t hash_val(const Types&... args)
{
    size_t seed = 0;
    hash_val(seed, args...);
    return seed;
}

class MyObject{
public:
    string a;
    char b;
    unsigned c;
    bool operator==(const MyObject& rhs) const
    {
        return a == rhs.a && b == rhs.b && c == rhs.c;
    }

    bool operator!=(const MyObject& rhs) const
    {
        return !operator==(rhs);
    }

    friend ostream& operator<<(ostream& os, const MyObject& ptr)
    {
        return os << ptr.a << "   " << ptr.b << "   " << ptr.c << endl;
    }
};

class MyHashFunction{
public:
    std::size_t operator()(const MyObject& obj) const {
        return hash_val(obj.a, obj.b, obj.c);
    }
};

unordered_map性能

加快 std::unorderd_map 的速度

为了避免动态改变哈希表的大小,可以预估容器中带插入元素的数量,并且预先申请好内存空间,避免动态分配过程中造成的 rehash 现象。当容器中的总元素超出了填充因子时,容器会重新申请更大的内存扩充桶的数量,然后重新哈希。

例如,我会预先调用 reverse 成员函数预先分配内存空间,以及设置好最大填充因子。

unordered_map<int,int> hash;
mp.reserve(1024);
mp.max_load_factor(0.25);

但是没什么用?

Google 开源的 abesil flat hash map

Google 则采用了开放寻址法中最简单的线性探测方法来解决哈希碰撞,取得了更快的速度。

开放定址法,当哈希表未满,在插入同义字时,可以把key值存放在下一个空位置(线性探测) 线性探测: 从发生冲突的位置开始,依次向后探测,直到寻找下一个空位置为止.

更快,解释开发寻址法,cache更优秀

高性能

https://github.com/greg7mdp/parallel-hashmap

https://github.com/greg7mdp/sparsepp

需要进一步的研究学习

暂无

遇到的问题

IPCC比赛的时候,发现用hash map会比红黑树的map慢。感觉不对劲,我hash空间声明大,冲突不就低了吗?

开题缘由、总结、反思、吐槽~~

参考文献

Parallel BFS

常规广度优先搜索的局限性

简单bfs是每次从队列中取出当前的访问节点后,之后就将它的邻接节点加入到队列中,这样明显不利于并行化。

实现一: 两队列

使用了两个队列,第一个队列存当前同一层的节点,第二个队列用来存储当前层节点的所有邻接节点(下一层节点),等到当前层的节点全部访问完毕后,再将第一个队列与第二个队列进行交换,即可。

这样做的优势在于便于以后的并行化。同一层的节点可以一起运行,不会受到下一层节点的干扰。

但是写有问题,并行计算要储存下一层节点时,push操作不是原子的,需要加锁。

q1.push(1);
   distance[1]=0;
   while(!q1.empty()){
      clear(q2);
      for(int i=0;i<q1.size();i++){
         int node=q1.front();
         q1.pop();
         cout<<node<<"->";
         int start_edge=g.outgoing_starts[node];
         int end_edge=(node==g.num_nodes-1)?g.num_edges:g.outgoing_starts[node+1];
         for(int j=start_edge;j<end_edge;j++){
               int outgoing=g.outgoing_edges[j];
               if(distance[outgoing]==-1){
                  distance[outgoing]=1;
                  q2.push(outgoing);
               }
         }
      }
      swap(q1, q2);
   }

实现二:PBFS

思想和实现一一样,但是对并行时竞争情况有处理。

思想:都是将并行处理的一层节点bag的相邻节点存到下一次处理的bag中。

竞争:

  1. 第 18 行的 v.dist 更新为 d+1。
  2. 幸运的是,由于都是设置为同一个值,该竞争不影响结果。
  3. 第19行,可能同时将同一个节点加入out-bag,导致额外的工作
  4. 但是这个bag数据结构是具有元素各异的性质,正好解决这个问题。
  5. 根本没有元素各异好吧。
  6. 第19行,可能同时将同一个节点加入out-bag,不会写同一个地方吗?insert

最后还是要加锁,屮。

辅助数据结构 - pennant

辅助数据结构,称为 "pennant", 是一个\(2^k\)个节点的特殊树。根只有个left子节点,该子节点有个完整是二叉树。

通过这个辅助数据结构,可以实现动态无序集的 bag 数据结构

注意,普通的k层完全二叉树,有\(\(1+2+4+2^{k-1}=2^k-1\)\)个节点。

PENNANT-UNION(x,y)
   1 y. right = x. left
   2 x. left = y
   3 return x
PENNANT-SPLIT(x)
   1 y = x. left
   2 x. left = y. right
   3 y. right = NULL
   4 return y

数据结构 - bags

bag是pennant的集合,其中没有相同大小的pennant。PBFS采用固定大小的数组S[0...r]称作 backbone 骨干。 数组中的第k元素S[k]为空或者保存着指向大小为\(2k\)的pennant。可知,整个数组S[0...r]最多保存\(2\)元素。 所以BAG-CREATE分配了保存空指针固定大小的数组backbone,会花费Θ(r)时间。

This bound can be improved to O(1) by keeping track of the largest nonempty index in the backbone.

BAG-Insert类似二进制计数器递增的过程

BAG-INSERT(S,x)
   1 k = 0
   2 while S[k] != NULL
      3 x = PENNANT-UNION(S[k],x)
      4 S[k++] = NULL
   5 S[k] = x                 #不执行循环 S[0] = x,不是在图7这种插入。
BAG-UNION(S1,S2)     
   1 y = NULL // The “carry” bit.
   2 for k = 0 to r
   3 (S1[k],y) = FA(S1[k],S2[k],y)
BAG-UNION(S1,S2)因为每一个PENNANT-UNION都需要固定的时间,计算FA(x,y,z)的值也需要恒定的时间。计算结果包主干中的所有条目需要Θ(r)时间。该算法可以改进为Θ(lgn),其中n是两个包中较小的包中的元素数,方法是保持每个包主干的最大非空索引,并将索引较小的包合并为索引较大的包

其中FA(x,y,z)相当于三个数的加法,c是进位,s是余位。

函数解释

函数时间开销

BAG-CREATE O(1) BAG-INSERT(n) O(1)-O(lgn) BAG-UNION(n) ~O(lgn)

函数时间开销

需要进一步的研究学习

暂无

遇到的问题

PBFS: 1. 11行为什么是lg呢,全部遍历不好吗? 1. 因为存的是\(2^r\)的元素

This bound can be improved to O(1) by keeping track of the largest nonempty index in the backbone.

开题缘由、总结、反思、吐槽~~

flood fill的需要

参考文献

https://www.cnblogs.com/JsonZhangAA/p/9016896.html

https://zhuanlan.zhihu.com/p/134637247

A Work-Efficient Parallel Breadth-First Search Algorithm (or How to Cope with the Nondeterminism of Reducers) from ACM SPAA 2010