Coursera 算法基础,北京大学
行的状态就均可从上一行推出
若最后一行的灯成功熄灭,证明该方案可行
2. 讨厌的青蛙
两点确定一条直线:枚举直线的起始两点确定直线
适当剪枝
递归
- 小游戏
- 棋盘分割
- 用栈模拟递归
- 完美覆盖:
需要仔细思考的一道 DP,我们可以发现对于一个矩形的完美覆盖可以有两种
一种可以将覆盖(竖着)分成两个子矩形,另一种则不能分割
为了不算重复情况,我们规定新增的矩形覆盖一定是不能分割的
观察到当 \(n=2\) 时,不能分割的覆盖情况有 \(3\) 种
而当 \(n>2\) 时则只有两种 (第 \(1\),\(2\) 行或第 \(2\),\(3\) 行交错)
基于此可以写出转移方程
#include <iostream>
using namespace std;
int dp[50];
int main() {
dp[0] = 1, dp[2] = 3;
for (int i = 4; i <= 30; i += 2) {
dp[i] += 3 * dp[i - 2];
for (int j = i - 4; j >= 0; j -= 2)
dp[i] += 2 * dp[j];
}
int n;
while (cin >> n) {
if (n == -1) break;
cout << dp[n] << endl;
}
return 0;
}
- 文件目录
栈模拟递归即可
动态规划
下限很低,上限巨高的算法
两个特点:最优子问题;无后效性
- 最长公共子序列 (LCS)
\(f[x][y]\) 一序列 \(1\) 至 \(x\) 二序列 \(1\) 至 \(y\) 的最长公共子序列的长度
复杂度 \(O(nm)\) - 最长上升子序列
\(f[x]\) 以第 \(x\) 个数位结尾的最长上升子序列长度
复杂度 \(O(n^2)\) - "人人为我" 与 "我为人人" 型递推:"人人为我"型有时可以采取数据结构优化
- 灌溉操场 (设计状态有点难)
- 方盒游戏
\(f[x][y][exlen]\) 消除第 \(x\) 个大块到第 \(y\) 个大块且右边只剩一个长度为 \(exlen\) 且与 \(y\) 同色的大块能得到的最高得分
\(f[x][y][exlen] = max(f[x][k][exlen + len(y)] + f[k + 1][y - 1][0], f[x][y - 1][0] + (len(y)+exlen)^2)\)
答案为 \(f[1][n][0]\) - 美丽栅栏
定义交叉序列为:每个数都同时大于或小于其相邻数的序列称为交叉序列
给 \(n\) 个数,求按字典序排列的第 \(c\) 个交叉序列
设 \(f[n][k][up/down]\) 代表有 \(n\) 个数,以第 \(k\) 小的数打头,且开头呈上升/下降趋势的交叉序列方案数
\(f[n][k][up] = \sum f[n - 1][k...n-1][down]\)
\(f[n][k][down] = \sum f[n - 1][1...k-1][up]\)
\(f[1][1][up]=f[1][1][down] = 1\)
计算出 \(f\) 数组后,求第 \(c\) 个排列即用枚举试错法即可
深度优先搜索 DFS
- 拯救少林神棍
之前也做过,有贼多剪枝,说实话自己想不出来 - 剪枝:可行性剪枝,最优化剪枝
- 做蛋糕
计算边界来进行可行性剪枝 - 碎纸机
简单深搜,状态和剪枝都很明确
广度优先搜索 BFS
- 八数码问题
很经典的广搜问题,之前也做过,但是这次听很有启发,关于保存八数码的状态
之前直接用 \(map\) 暴力存状态过的,这样虽然方便,但是颇有些名不正言不顺之感
如果以字符串形式保存八数码,占太多空间,难以接受 (除非用 \(map\),这样无用的状态不会占用空间)
将八数码视为排列,将每一种方案映射成排列中的序数
如 \(0123456789\) 映射为 \(0\) 因为它是第 \(0\) 个排列,这样就实现了状态的保存 (感觉有一点离散化的思想在里面) - 八数码问题解的判定
八数码问题的状态实际上是 \(0\) 至 \(8\) 的一个排列,对于任意初始状态到目标状态,不一定存在解路径
因为排列有奇排列与偶排列两类,可以确定奇排列不能转化为偶排列,偶排列不能转化为奇排列
对于一个随机排列,定义 \(f(x) (x \neq 0)\) 为数字 \(x\) 之前比它小的数的个数。全部数字的 \(f(x)\) 之和定义为 \(y = \sum f(x)\)
若 \(y\) 为奇数,则排列为奇排列;反之为偶排列 - 双向广搜 \(DBFS\)
从起始节点与目标节点以广度优先顺序同时扩展,扩展队列相交后可视为找到一条解路径
与 \(BFS\) 相比,搜索树宽度明显减小
在扩展时,总是选择扩展结点数量较少一侧进行扩展,以保证两个扩展队列的大小相仿 - \(A*\) 算法
- 还有一个操作以前没用过:广搜时存储每一个节点的父节点指针,由此在达到目标状态之后可以输出解路径
- 广搜 vs 深搜
广搜:对于状态简单,空间充足的求最优解问题有优势(完备策略)
深搜:几乎适合一切问题,在特定问题上时间逊于广搜 - \(Flip Game\)
因为一个大小写 \(WA\) 了我一个小时...
后来看到题解有更加快速且简介的做法:由于 \(n=4\), 共有 \(4 \times 4 = 16\) 个 \(0/1\),完全可以二进制压缩存储
翻棋子即可视为与某一个二进制数异或:需要翻转的位设为 \(1\) ,不需要翻转的设为 \(0\)
二分与贪心
- 雷达安装问题
对这个题很有印象,以前绝对做过(果然,在洛谷上找到了 P1325)
将每个小岛对应的的雷达安装范围作为线段储存,问题转化为经典的线段覆盖问题:
数轴上给出若干线段,选择最少的 \(n\) 个点使得每一根线段都包含至少一个点
贪心策略很容易理解:将线段按照左端点排序并维护一个线段集合,记录该集合的最左 右端点
可以发现,这个最左右端点可以覆盖此集合内的所有线段
若新添加的线段左端点大于线段集合的最左右端点,说明之前的点已经无法覆盖到当前这条线段:那么重新创立一个线段集合
否则将其加入原有的线段集合,并更新最左右端点 - 补充另外一个线段覆盖问题:
数轴上给出若干线段,要求选择其中 \(k\) 条两两不相交的线段,使得 \(k\) 最大
贪心策略:放右端点最靠左的线段最好,从左向右放,右端点越小妨碍越少
其他线段放置按右端点排序,贪心放置线段,即能放就放 - Gone fishing
写的 \(DP\) 过了样例,不知道为什么 \(WA\) 了,而且网上搜的标程也 \(WA\) 了
看了题解,这个贪心确实很妙
枚举目的地湖,并将时间减去移动需要的时间,这样可以视为能在任意湖之间 "瞬间移动"
然后在这些湖当中贪心的选择:哪个湖鱼多就选哪个湖,直至时间耗尽:若鱼钓完之后还有时间,则全部分配给第一个湖