算法基础课程的总结,方便以后快速查阅和复习
Week 2 枚举
基本方法
列举各元素,进行猜测。
- 给出解空间,建立简洁的数学模型,列举出可能的情况
- 减小搜索的空间,避免不必要的计算
- 采用合适的搜索顺序
经验总结
输入:
scanf("%d", &cases);
输出:
printf("PUZZLE #%d \n", i+1);
对一行元素进行枚举的一种常见方法:模拟二进制加法,处理进位
比如,对press第一行元素的取值(0/1)进行枚举:while(guess() == false){ //结束的条件 press[1][1]++; //每次循环从从最低位加1(不考虑press[1][0]) c = 1; //从最低位开始,准备进位 while (press[1][c] > 1) { //如果当前位超过1 press[1][c] = 0; //当前位置0 c++ ; //进位 press[1][c]++; } }//这样将不断地往后增加,直到满足结束条件,普通循环嵌套不如这个方便
很多情况下,元素太多,难以直接枚举,这时冷静下来,先想好策略,再开始敲代码
每次取出两个元素,遍历所有取法:
for (int i = 0; i < n - 1; i++) for (int j = i + 1; j < n; j++) ...
有时定义个结构体还是很方便的:
struct PLANT{ int x, y; }
Week 3 递归
基本方法
递归:某个函数直接或间接地调用自身。
求解过程:将问题划分为许多相同性质的子问题,这些子问题的递归求解就构成了原问题的解。
原问题为f(x),想尽办法寻找函数g(x),使得f(x) = g(f(x-1)),其他形式也行,只要找出了这样的迭代的规律,就能就能将f(x)不断分解,直到“出口”,比如已知了f(0)的值。
- 递归的三个要点
- 递归式:如何将原问题划分为子问题
- 递归出口:递归终止的条件,允许多个出口
- 界函数:问题规模变化的函数,保证递归的规模向出口条件靠拢
注意事项:函数的局部变量存在栈上,递归很深的时候,各个子问题的函数的局部变量可能导致栈溢出
经验总结
迷宫求解问题,每一步的探测方式相同(自相似性),可以使用递归
主要考虑:判断是否符合要求,如何递归,需要记录那些数据
递归式可能是:\(f_{(n)}(·) = f_{(n-1)}(·) + f_{(n-2)}(·)\)等各式各样的,需要根据实际问题发散思考
使用记录表(在递归计算的过程中想办法多存下一些有用的东西),减少之后的计算量
int res[15][9][9][9][9]
初始化记录表:
memset(res, -1, sizeof(res))
那些重复的按照某些特定规律进行操作从而解决问题的方法,先用人脑想出操作过程,然后用递归来实现
Week 4,Week 5 动态规划
有时使用递归来求解问题时,子问题可能存在大量重复计算,从而严重影响效率,除了在递归计算的过程中使用记录表存一些数据,还可以使用递推的方式:从简单的状态开始递推到要解决的问题。
基本方法
- 将原问题分解成子问题
- 确定状态:一个或多个子问题各个变量的一组取值
- 确定一些初始状态的值
- 确定状态转移方程
- 适用的情况
- 最优化原理:最优解所包含的子问题的解也是最优的
- 无后效性:状态确定后不会受以后影响,这样才能正常递推
- 动归的三种形式:
- 记忆递归型
- ”我为人人“递推型
- ”人人为我“递推型
经验总结
#include <algorithm>
sort(A, A+a);//从小到大
可以直接使用
max(a, b)
得到最大值。当选取的状态,难以进行递推时,考虑将状态增加限制条件后分类细化,即增加维度,然后在新的状态上尝试递推
状态往往是很多的值(数组),使用合适的顺序不断更新状态。
关键还是在于找到问题的递推方式。
Week 6, Week 7 深度优先搜索
基本方法
深度优先搜索遍历整个图的框架为:
Dfs(v){
if(v访问过)
return;
do something;
将v标记为访问过;
对和v相邻的每个点u: Dfs(u);
//程序运行的时候,将会深入某一个分支直到这个分支全部访问完,才会进入下一个分支。
}
int main(){
while(还能找到未访问的点k)
Dfs(k);
}
经验总结
图的每个结点常常有很多相关数据,可以定义个结构体来带代表每个结点
struct Room {int r, c; Room(int rr, int cc): r(rr), c(cc) {} };
保存一系列的结点信息,这时可以使用vector:
struct Road{ int d, L, t; }; vector<vector<Road>> cityMap(100);
遍历整个图常常很费时,面对具体问题,应该想尽办法尽早判断该结点或分支是否值得继续搜索(剪枝)
Week 8 广度优先搜索
有时不需要遍历整个图,比如希望找到符合条件的步数最少的节点,可以给节点分层,一层一层地去判断,找到了合适的就停止。
基本方法
广度优先搜索算法(使用queue)
- 把初始节点S0放入Open表中
- 如果Open表为空,则问题无解,失败退出
- 把Open表的第一个节点取出放入Closed表,并记该节点为n
- 考察节点n是否为目标节点,若是,则得到问题的解,成功退出
- 若节点n不可扩展,则转到第2步
- 扩展节点n,将其不在Close表和Open表中的子节点(判重)放入Open表的尾部,并为每一个子节点设置指向父节点的指针或记录节点的层次,然后转到第2步
经验总结
- 使用队列
#include <queue>
queue<Step> q
q.push(Step(N, 0))
Step s = q.front()
q.pop
判重常常需要根据具体情况,建一个标志位序列。根据时间空间的权衡设计合适的标志位序列。
给定排列求序号
数出有多少种排列比给定的排列小,比如3241:
- 1, 2放在第一位,有2*3! = 12种
- 3在第一位,1放在第2位,有 2! = 2种
- 32放在前面,第三位可以放1,有 1种,然后就没有其他的了
给定序号n求排列
- 第一位假定是1,共有3!种,没有到达9,所以第一位至少是2
- 第一位是2,一共能数到 3!+3!号,>= 9,所以第一位是2 第二位是1,21??,一共能数到 3!+2! = 8 不到9,所以第二位至少 是 3
- 第二位是3,23??,一共能数到 3!+2!+2! >= 9,因此第二位是3
- 第三位是1,一共能数到3!+2!+1 = 9,所以第三位是1,第四位是 4
- 答案:2314
八数码问题,从奇排列不能转化成偶排列或相反。很多问题如果可以人为的找到一些规律,利用这些规律常常能简化搜索过程。
bitset的使用
#include <bitset>
bitset<362880> Flags;
Flags.reset();
if(Flags[n]) continue;
Flags.set(n, true);
求和
#include <numeric>
int A[16] = {0}; int sum = accumulate(A, A + 16, 0);
//最后那个0是指求和的初值赋值,将内容拷贝到想要放置的内存位置
#include <string.h>
memcpy(A, AA, sizeof(A));
可以直接使用一个int型的整数来模拟二进制的序列,比如
Int x = 0xffff;
操作的时候使用为运算符
Week 9 二分与贪心算法
基本方法
二分查找
对已经排序好的序列,首先检查序列的中间元素:
- 如果大于这个元素,在当前序列的后半部分继续查找
- 如果小于这个元素,在当前序列的前半部分继续查找
- 知道找到相同的元素,或者所查找的序列范围为空为止
贪心算法
问题求解时,总是做出在当前看来是最好的选择(不保证全局最优)
很多看上去很复杂的问题可以设计成贪心算法能解决的形式,设计贪心策略判断是否满足“无后效性”
经验总结
对自定义的对象排序
bool operator < (const Box &a, const Box &b){ return a.density < b.density; } sort(boxes, boxes + n); bool comp(const int &a,const int &b){ return a>b; } sort(v.begin(),v.end(),comp);
简化问题:最小值问题-->判定性问题
- 求某个最小的情况,可以从
i = 0
开始,判断i = x
是否能满足条件,但是这样效率太低,为了提高效率,考虑采用二分法来查找 - 经典思想:二分+判定
- 求某个最小的情况,可以从
设计贪心策略常常可以对序列先进行排序,然后再依次进行操作
作者:rubbninja
出处:http://www.cnblogs.com/rubbninja/
关于作者:目前主要研究领域为机器学习与无线定位技术,欢迎讨论与指正!