文章目录
二分法
参照二分查找。
递归
递归通俗来讲就是不断调用自身。
递归只是让解决方案更加清晰,并没有性能上的优势。
如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。
递归有三个基本组成部分,分别是执行的内容(可省略)、基线条件(函数不再调用自己)以及递归条件(函数调用自己)。
int factorial(int x){
// 基线条件
if (x == 1)
return x;
// 递归条件
else
return x * fab(x);
}
递归在使用过程中可能占用大量的内存(调用栈),此时有两种解决方案:
- 重新编写代码,转而使用循环;
- 使用尾递归。
分而治之(D&C,divide and conquer)
分而治之策略是递归的,包括了两个过程:
- 找到基线条件,这种条件必须尽可能简单;
- 不断将问题分解(或者说缩小规模),直到符合基线条件。
分而治之是基于递归的,更是一种解决问题的思想,递归偏于解决问题的策略/手段。
以对数组中的数求和为例:
首先,我们需要找到基线条件:数组为空时返回0或者数组只有一个元素时返回该元素;
接着,我们需要对问题进行分解,即每一次递归都将数组的最后一个元素+sum(剩下的元素构成的数组)。因为,传递给sum的数组变得简单了,这也是一种问题规模的缩减。
int sum(vector<int> seq){
if seq.empty()
return 0;
else if (seq.size == 1)
element = seq.back();
seq.push_back();
return element + sum(seq);
}
贪婪算法
贪婪算法的本质:每步都选择局部最优解,最终得到的就是全局最优解。
并非在任何情况下都行之有效。
背包问题
有一个小偷,只偷最贵的东西,但是他的包只能装下35磅的物品。
此时,在他眼里,有三样物品:
物品 | 重量/kg | 价格/¥ |
---|---|---|
音响 | 30 | 3000 |
笔记本电脑 | 20 | 2000 |
吉他 | 15 | 1500 |
显而易见,按照贪婪算法的要求,小偷无法使自己的利益最大化,即得不到最优解。
集合覆盖问题
问题是:一共有八个州,四个电台,希望能选择尽可能少的电台将这八个周覆盖了。
4个电台就有 2 4 2^4 24个集合需要考虑,那么枚举的话,时间为 O ( 2 n ) O(2^n) O(2n)。
为了加快解题速度,解题思路是:每次都选择覆盖多的。
这是一种近似算法,得到近似解。
#include <iostream>
#include <map>
#include <string>
#include <set>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
// 电台信息
map<string,vector<string> > stations;
stations["kone"] = { "id", "nv", "ut" };
stations["ktwo"] = { "wa", "id", "mt" };
stations["kthree"] = { "or", "nv", "ca" };
stations["kfour"] = { "nv", "ut" };
stations["kfive"] = { "ca", "az" };
// 初始化
// 当前未覆盖的州
vector<string> states_needed = { "mt", "wa", "or", "id", "nv", "ut", "ca", "az"};
// 候选电台列表
vector<string> final_stations;
// 选中电台
string best_station;
// 选中电台所覆盖的州
vector<string> states_covered;
// 贪心算法
while (!states_needed.empty()) {
// 遍历电台,找最优
for (auto& s : stations) {
// 候选电台与未覆盖州的交集
vector<string> covered;
sort(states_needed.begin(), states_needed.end());
sort(s.second.begin(), s.second.end());
set_intersection(states_needed.begin(), states_needed.end(), s.second.begin(), s.second.end(), back_inserter(covered));
if (covered.size() > states_covered.size()) {
best_station = s.first;
states_covered = covered;
}
}
// 记录选中电台
final_stations.push_back(best_station);
// 删除选中电台
stations.erase(best_station);
// 删除已经覆盖的州
set<string> states_needed_set(states_needed.begin(), states_needed.end());
for (auto& s : states_covered)
states_needed_set.erase(s);
states_needed.assign(states_needed_set.begin(), states_needed_set.end());
// 重置
best_station = {};
states_covered = {};
}
for (auto& s : final_stations)
cout << s << endl;
return 0;
}
在写代码的过程中,有以下几个注意点:
- 一些参与比较,时常更新的值要注意是否每个循环都要初始化;
- 使用交集算法
set_intersection
时,需要注意的有:将两个集合排序;通过push_back插入结果,不是赋值;选取支持push_back的容器。
NP完全问题
没有办法判断问题是不是NP完全问题(没有快速算法的问题),但是是有一些蛛丝马迹可循的:
- 元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变的非常慢;
- 涉及“所有组合”的问题通常是NP完全问题;
- 不能将问题分成小问题,必须考虑各种可能的情况。这可能是NP完全问题;
- 如果问题可转化为集合覆盖问题或旅行商问题,则肯定是NP完全问题。
NP完全问题可以通过近似算法得到近似解。
动态规划
再探背包问题
动态规划问题和NP完全问题的目的都是为了找一个最优解,但区别在于,动态规划问题看似是个NP问题,但是其可以分解成小问题。
比如上文我们描述的背包问题。
我们可以先解决子背包问题,再逐步解决原来的问题。
我们设背包承重4磅。吉他1磅,1500美金;音响4磅,3000美金;笔记本电脑3磅,2000美金。
我们首先给出一个网格:
1 | 2 | 3 | 4 | |
---|---|---|---|---|
吉他 | ||||
音响 | ||||
笔记本电脑 |
从左到右,表示逐步扩大的背包承重数;从上到下,表示可供选择的物品的增加。
每一格的含义是,在x的承重下,从y(该行及往上的物品)中偷盗物品可以获得的最大金额。
于是,这张表格可以写成:
1 | 2 | 3 | 4 | |
---|---|---|---|---|
吉他 | 1500 | 1500 | 1500 | 1500 |
音响 | 1500 | 1500 | 1500 | 3000 |
笔记本电脑 | 1500 | 1500 | 2000 | 3500 |
该表格的公式为(i表示行,j表示列):
C
E
L
L
[
i
]
[
j
]
=
max
(
C
E
L
L
[
i
−
1
]
[
j
]
(
上
一
个
单
元
格
的
值
)
,
当
前
商
品
的
价
值
+
C
E
L
L
[
i
−
1
]
[
j
−
当
前
商
品
的
重
量
]
)
CELL[i][j]=\max(CELL[i-1][j](上一个单元格的值),当前商品的价值+CELL[i-1][j-当前商品的重量])
CELL[i][j]=max(CELL[i−1][j](上一个单元格的值),当前商品的价值+CELL[i−1][j−当前商品的重量])
这其实就是一个有约束条件的优化问题,约束条件为背包承重,优化对象为偷盗的钱财。
细节补充
- 如果再增加一个物品,不过是给这个表格再加一行之后套公式求解;
- 如果补充了一件0.5磅的物品,那么考虑的粒度更细,我们网格也要更细;
- 对于商品,只能一整件,不能说偷商品的百分之多少;
- 每个子问题都是离散的,不存在依赖关系。
设计动态规划模型时的注意:
- 每种动态规划都涉及网络;
- 单元格中的值通常是我要优化的值,
- 每个单元格都是一个子问题,因此你应考虑如何将问题分成子问题。
计算编辑距离的过程也是动态规划的思路!