0
点赞
收藏
分享

微信扫一扫

算法思想(持续更新...)

天蓝Sea 2022-02-07 阅读 32

文章目录

二分法

参照二分查找。

递归

递归通俗来讲就是不断调用自身。

递归只是让解决方案更加清晰,并没有性能上的优势。

如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。

递归有三个基本组成部分,分别是执行的内容(可省略)、基线条件(函数不再调用自己)以及递归条件(函数调用自己)。

int factorial(int x){
	// 基线条件
	if (x == 1)
		return x;
	// 递归条件
	else
		return x * fab(x);
}

递归在使用过程中可能占用大量的内存(调用栈),此时有两种解决方案:

  1. 重新编写代码,转而使用循环;
  2. 使用尾递归。

分而治之(D&C,divide and conquer)

分而治之策略是递归的,包括了两个过程:

  1. 找到基线条件,这种条件必须尽可能简单;
  2. 不断将问题分解(或者说缩小规模),直到符合基线条件。

分而治之是基于递归的,更是一种解决问题的思想,递归偏于解决问题的策略/手段。

以对数组中的数求和为例:

首先,我们需要找到基线条件:数组为空时返回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价格/¥
音响303000
笔记本电脑202000
吉他151500

显而易见,按照贪婪算法的要求,小偷无法使自己的利益最大化,即得不到最优解。

集合覆盖问题

问题是:一共有八个州,四个电台,希望能选择尽可能少的电台将这八个周覆盖了。

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;
}

在写代码的过程中,有以下几个注意点:

  1. 一些参与比较,时常更新的值要注意是否每个循环都要初始化;
  2. 使用交集算法set_intersection时,需要注意的有:将两个集合排序;通过push_back插入结果,不是赋值;选取支持push_back的容器。

NP完全问题

没有办法判断问题是不是NP完全问题(没有快速算法的问题),但是是有一些蛛丝马迹可循的:

  1. 元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变的非常慢;
  2. 涉及“所有组合”的问题通常是NP完全问题;
  3. 不能将问题分成小问题,必须考虑各种可能的情况。这可能是NP完全问题;
  4. 如果问题可转化为集合覆盖问题或旅行商问题,则肯定是NP完全问题。

NP完全问题可以通过近似算法得到近似解。

动态规划

再探背包问题

动态规划问题和NP完全问题的目的都是为了找一个最优解,但区别在于,动态规划问题看似是个NP问题,但是其可以分解成小问题。

比如上文我们描述的背包问题。

我们可以先解决子背包问题,再逐步解决原来的问题。

我们设背包承重4磅。吉他1磅,1500美金;音响4磅,3000美金;笔记本电脑3磅,2000美金。

我们首先给出一个网格:

1234
吉他
音响
笔记本电脑

从左到右,表示逐步扩大的背包承重数;从上到下,表示可供选择的物品的增加。

每一格的含义是,在x的承重下,从y(该行及往上的物品)中偷盗物品可以获得的最大金额。

于是,这张表格可以写成:

1234
吉他1500150015001500
音响1500150015003000
笔记本电脑1500150020003500

该表格的公式为(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[i1][j](),+CELL[i1][j])
这其实就是一个有约束条件的优化问题,约束条件为背包承重,优化对象为偷盗的钱财。

细节补充

  1. 如果再增加一个物品,不过是给这个表格再加一行之后套公式求解;
  2. 如果补充了一件0.5磅的物品,那么考虑的粒度更细,我们网格也要更细;
  3. 对于商品,只能一整件,不能说偷商品的百分之多少;
  4. 每个子问题都是离散的,不存在依赖关系。

设计动态规划模型时的注意:

  1. 每种动态规划都涉及网络;
  2. 单元格中的值通常是我要优化的值,
  3. 每个单元格都是一个子问题,因此你应考虑如何将问题分成子问题。

计算编辑距离的过程也是动态规划的思路!

举报

相关推荐

0 条评论