qbxt 刷题班基础算法笔记
基础算法
- 枚举
- 分治
- 搜索
- 贪心
枚举/搜索
简单的枚举
- 给定 \(n (n \leq 15)\) 输出{1,2,3,4……n} 的所有子集 (\(2^n\))个
用二进制存每个数的状态,每一位有和没有代表这个数选还是不选。
int s = (1 << n) - 1;
for (int s0 = s; s0; (s0 - 1) & s)
- 给定一个 \(n (n\leq 15)\) 输出{1, 2, 3, 4……n}的所有子集,以及所有子集的子集。
复杂度:\(O(3^n)\)
int n;
for (int i = 0; i < (1 << n); i++)
for (int j = i; j > 0; j = (j - 1) & i)
Meet in the middle
T1
题目大意:
有四个整数数列,每个数列包含 \(n\) 个整数。\((n\leq1000)\) 。现在从四个数列中各选一个数,使得四个数和为0,问有多少种方案。选的数在不同位置,就算不一样的选法。
solution
先处理前两个序列,\(n^2\) 枚举两个数,开个 \(map\) ,把所有出现出现的加和的次数记录下来,然后枚举后两个序列的两个数,把它们加和在 \(map\) 的找到它的相反数出现的次数记录答案就好了。
T2
题目大意:\(n\) 个数字,找出两两和的第 \(k\) 大。\(n\leq 10^6\)
solution
对序列排个序,可以二分第 \(k\) 大的权值为 \(x\) ,然后判断比 \(x\) 小的点对数量是否等于 \(k\)。判断可以 \(O(n)\) 实现:因为排完序了,所以满足单调性,双指针,一个从前往后扫,另一个从后往前扫,直到与第一个指针的权值小于 k,然后就可以记录答案了。
T3
\(n\) 个数字,找出 \(2^n\) 个子集和里的第 \(k\) 大 \(n\leq 30\)
solution
把序列分为 2 部分,每部分 15 个数,那么两部分都可以生成出来了,然后就可以判断了
T4
有 \(n\) 个箱子,每个箱子打开有 \(p_i\) 的概率,获得 \(v_i\) 的金币,\(1 - p_i\) 的概率获得 1 个钻石
这个人打开箱子之后,会拿这些金币和钻石区买东西,\(m\) 个东西,第 \(i\) 个东西需要 \(c_i\) 的金币,\(D_i\) 钻石购买,问购买的东西数量的最大值的期望。
\(2\leq n \leq 30\),\(1\leq m \leq 30\),\(1 \leq v_i,c_i\leq10^7\)
\(0\leq D_i\leq 30\) ,\(0\leq p_i\leq 100\)。
solution
\(n\leq 20\) 的时候可以直接枚举每个箱子,然后带着这些金币和钻石去买东西。
买东西是个有两种货币的背包问题。
发现钻石数量很少,但金币很多。
把钻石和买的物品的个数当作背包的状态,求在该状态下花费金币最少为多少。
枚举钻石和物品,求出在一定钻石和物品的条件下的最小金币花费。
当 \(n\leq 30\) 的时候,就要考虑开箱的过程。
把箱子分成两部分,每部分 15 个。
枚举一部分箱子的开箱情况,然后这部分的箱子的金币,钻石和概率就都可以得到了,然后枚举总的钻石和物品,另一部分箱子贡献的钻石就得到了,在背包里查一下这个状态所需金币的数量,就可以确定另一部分箱子贡献金币的最少数量,物品 +1 的所需金币就是另一部分箱子所贡献金币的最大数量(贡献多了有可能物品就不满足了),然后另一部分箱子就有了贡献的钻石和金币的范围。对左边的箱子按钻石和金币排序进行排序,就可以查询数这部分箱子的概率,总的概率*箱子数就是答案
分治
- 归并排序
- 快速幂,慢速乘
- 二分查找,二分答案
- 分治
单调性
最优性-> 判定性问题
整数二分
//L是最后一个符合条件的,R是第一个不符合
int L = 1, R = n;
while (R - L > 1) {
int mid = R + L >> 1;
if (check(mid)) L = mid;
else R = mid;
}
实数二分
//实数二分
double L = 1, R = n,eps = 1e-5;
while(R - L > eps) {//for(int t = 0; t < 100; t++)防止死循环
double mid = (R + L) / 2;
if (check(mid)) L = mid;
else R = mid;
}
STL lower_bound (>=)/ upper_bound (>)
实数二分可能会有死循环,可以记录一个二分的次数,超过某个次数就跳出。
T1
题目大意:平面上 \(n\) 个点,找出一对点的距离最小。
\(n\leq 200000\)
solution
最暴力的思路就是 \(n^2\) 枚举点对,显然过不了。
考虑分治。
先考虑横坐标。
把所有点按照横坐标排个序,把点从中间分为两部分,递归下去就可以求得每部分的答案。
考虑合并,显然两部分最小值直接取 min 是不合法的,可能会在两部分的交界处产生新的答案。
设左边部分的答案为 \(d_1\) ,右边的答案为 \(d_2\) ,另 \(d = min(d_1, d_2)\)
中间的划分线为 \(m\),则左边横坐标在 \(m - d\) 内的点最多只有一个,右边在 \(m + d\) 的点也只有一个(忽视纵坐标),这样就可以 \(O(1)\) 更新答案了
现在考虑纵坐标,可以先把纵坐标排个序,然后维护两个指针可以从下往上扫,可以 \(O(n)\) 的合并点对了。
复杂度: \(O(n~logn)\)
分数规划
最优比例生成环/树
题目大意:
在一个 \(n\) 个点 \(m\) 条边的无向图寻找一个环,使得这个环的点权和除以边权和最大。假设所有的权值都为正数。
\(n\leq 1000, m \leq 5000\)
转化题意,也就是求 \(max\frac{\sum a_i}{\sum b_i}\)
二分答案 \(c\)
怎么判断这个 \(c\) 合不合法 ?
把 \(c\) 代入式子 \(\frac{\sum a_i}{\sum b_i}\geq c\) ,化简得到 \(\sum a_i - c\sum b_i\geq 0\)
再化简:\(\sum (a_i - c\times b_i)\)
就把原来的只有所选的点全部确定才直到答案转化到每个点的贡献已知,看答案是否合法。
回到这个题,答案一定是简单环。(一个八字形的环一定不如其中的一个小环更优)
所以就可以把点权转化到边权上,跑 dfs 判断是否有解就好了(简单环每个点只对应一个出边/入边)。
(POJ2728)题目大意: 有 \(n\) 个村庄,村庄在不同坐标和海拔,现在对所有村庄供水,只要两个村庄之间有路即可,建造水管的距离为坐标之间的欧几里得距离,费用为海拔之差,现在要求方案使得费用与距离的比值最小。
solution
同样思路,二分一个答案,处理所有的边,然后看是否能形成一个生成树满足答案。**
扩展(乘积规划)
求 \(min \sum a_i\times\sum b_i\) ?
结合到二维平面上求解??
点分治
POJ 1741 题目大意:
给定一棵 \(n\) 个节点,边上带权的树,再给出一个 \(k\) ,询问有多少个数对 \((i, j)\) ,满足 \(i<j\) ,且 \(i\) 与 \(j\) 两点在树上的距离小于等于 \(k\) 。
solution
点分治。
先解决这么一个问题:求经过给定点的点对间路径小于等于 \(k\) 的点对数量有多少 ?
把给定的点当作根,然后 \(dfs\) 求出根到每个点的路径,标记每个点在哪个子树(根的哪个儿子)内,那么剩下的就是求所有 \(dis[i] + dis[j] \leq k\) 的点对的数量,保证 \(i\) 和 \(j\) 在不同的子树内;
容斥。先算出这棵树符合条件的路径数,然后再删掉同一个子树内的合法路径的点对的数量,合法路径可以用双指针 \(O(n)\) 的求出来,求距离根节点的距离 \(logn\) ,复杂度 \(O(n log n)\)
这样就解决了经过一个点的路径长度不超过 \(k\) 的所有点对的数量。
不经过这个点的合法的点对?可以递归,可看作,把根节点删掉,用相同步骤处理子树就好了
怎么找这个给定的点?
如果默认点的话,链的情况就会被链卡成 \(n^2\) ,所以每次都找重心就好了,这样分治的复杂度就是 \(O(logn)\)
总的复杂度为 \(O(n log^2n)\)
CDQ (基于时间的)分治
COGS 577 蝗灾
题目大意:给定一个 \(W\times W\) 二维的平面,现在进行两个操作:
- 对平面内的一个单点进行加操作
- 查询一个矩阵内所有点的权值和
solution
子问题:如果查询都在后面的话?(AAAAA……QQQQQ)
先对纵坐标建一棵线段树,然后维护一个扫描线,从左向右扫,每扫到要修改的点就对把这个点对应的纵坐标在线段树上进行修改,每个查询操作对两个端点的线段树区间求和,然后作差就好了。
如果查询和修改打乱顺序捏?
对其进行分治,左区间的修改操作一定会对右边的查询操作产生影响,所以每分一层就相当于就把所有的左区间的修改操作和右区间的查询操作放在一起,就可以在每一层做一个上面的子问题,复杂度:\(O(nlog^2n)\)
贪心
哈夫曼树
带权路径长度最小的二叉树
树的路径长度:每个叶子节点到根节点长度之和
树的带权路径长度(WPL):\(\sum\) 每个叶子结点的路径长度 \(\times\) 每个叶子结点的权值
哈夫曼树就是使得树的带权路径长度最小。
构建过程:每次选择集合中两个权值最小的点,加和得到一个数当作这两个点的父亲,在集合中删掉这两个点,把两点的和加入到集合中,重复此操作,直到集合为空。
k 叉哈夫曼树
现在要构造一棵 \(k\) 叉哈夫曼树 \((k > 2)\)
如果同上,每次选 \(k\) 个点合并成一个,合并到最后,有可能根节点的儿子就不够 \(k\) 个了,这时这个 \(k\) 叉哈夫曼树显然不符合带权路径长度最小了(从叶子节点任选一个移到根节点当儿子一定会更优)。
每次合并减少 \(k - 1\) 个,只有满足 \(k - 1 | n - 1\) 的时候才能正好合并,对于那些不满足条件的情况可以向图中不断添加权值为 \(0\) 的点使它满足这个条件,权值为 \(0\) 的点肯定会先合并,这样就可以使得有权节点的权值深度降低。
有一个由 \(n\) 种字符组成的字串,给出这 \(n\) 种字符的出现次数,用 \(k\) 进制数来表示这个字符串。
求出这个 \(k\) 进制数的最短长度,并求出此时编码最长的字符的最短编码长度。
solution
每个字母出现的次数当作权值,建立 \(k\) 叉哈夫曼树。
第一问就是 WPL ,第二问就是求深度最大的点的深度
具体实现:一个优先队列,维护每个点的权值和高度,权值为第一关键字,高度为第二关键字,如果不符合 \(k - 1 | n - 1\) 就先不断向队列中加入 0 点,然后每次选 k 个点合并,依次统计 WPL,直到队列只剩一个点。
区间选择
给定 \(n\) 个形如 \([a_i, b_i]\) 的区间,最多能选出多少个互不相交的区间。
solution
贪心,按照右端点排个序,依次选择,如果当前点的左端点小于上一次选择的右端点,就不可以选。
区间点覆盖
数轴上有 \(n\) 个闭区间 \([a_i, b_i]\)。
取尽量少的点,使得每个区间内都至少有一个点。
solution
按照每个区间的右端点从小到大排序,判断上一次选的点在不在该区间内,如果不在就选右端点。
一个数列,选 \(k\) 个点对,并且点对两两之间没有交集,最小化所有点对之间的距离。
solution
贪心的反悔机制。
选择的点一定是相邻的点。计算出相邻点的距离,看作一个个的有权值点,要求就是选出不相邻的点使得总权值最小。
每次选权值最小的点是不对的。样例:2 1 2 9
考虑可撤销机制。
还是每次选择最小的点,选完之后把该位置替换成它相邻的两个点的和 - 该点权值,同时把它相邻的两个点标记一下,这样就实现了可撤销操作。
具体实现用双向链表和优先队列来实现就好了。
搜索
solution
最优性剪枝:已算得的答案(最小面积)减去当前层一下的层的面积是否小于上面的层所 能构成的最小面积,如果小于则返回。
可行性剪枝:总的体积减去当前层一下的总体积是否小于上面的层所能构成的最小体积,如果小于则返回。