算法设计与分析期末抱佛脚复习

Posted karshey

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法设计与分析期末抱佛脚复习相关的知识,希望对你有一定的参考价值。

写在前面:
是抱佛脚复习,不全。
知识和代码没有标出处的都来自书上:《计算机算法设计与分析》王晓东

第1章 算法概述

算法的四条性质

  1. 输入:有0个或多个输入
  2. 输出:至少产生一个输出
  3. 确定性:组成算法的每条指令是无歧义的
  4. 有限性:每条指令的执行次数和时间是有限的

算法复杂性依赖的三个要素

算法时间复杂度:最好、最坏、平均
最具有实际价值的是 最坏情况下的 时间复杂度。

关于时间复杂度的基本要点

1.时间复杂度反映的是随着问题规模的变大,计算所需的时间的增长速度,与系数的多少关系不大
2.算法的渐近时间复杂度,简称时间复杂度,很多时候为了便于理解,直接把时间复杂度等同于O()是可以的。
3.常见的时间复杂度,及其增长速度比较
O(1)<O(log n)<O(n)<O(nlog n)<O(n2)<O(n3)< O(2n)<O(n!)<O(nn)

大O运算规则:
(1) O(f)+O(g)=O(max(f, g))
(2) O(f)+O(g)=O(f+g)
(3) O(f)O(g)=O(fg)
(4) 如果g(N)=O(f(N)), 则O(f)+O(g)=O(f)
(5) O(Cf(N))=O(f(N)), 其中C是一个正常数
(6) f=O(f)
来自这里

P和NP问题

什么是P类问题?
多项式时间算法,即对规模为n的输入,算法在最坏情况下的时间复杂度为 O(nk)
所有在多项式时间内能够求解的判定问题构成P类问题

什么是NP类问题?
P类问题是确定性计算模型下的易解问题类,而NP类问题是非确定性计算模型下的易验证问题类。(N即non-…)

NP类问题:能在多项式时间内验证得出一个正确解的问题。(NP:Nondeterministic polynominal,非确定性多项式)
P类问题是NP问题的子集,因为存在多项式时间解法的问题,总能在多项式时间内验证他。
注意定义,这里是验证。NP类问题,我用个人的俗话理解就是,不知道这个问题是不是存在多项式时间内的算法,所以叫non-deterministic非确定性,但是我们可以在多项式时间内验证并得出这个问题的一个正确解。
举个例子,著名的NP类问题:旅行家推销问题(TSP)。
即有一个推销员,要到n个城市推销商品,他要找出一个包含所有n个城市的环路,这个环路路径小于a。我们知道这个问题如果单纯的用枚举法来列举的话会有(n-1)! 种,已经不是多项式时间的算法了,(注:阶乘算法比多项式的复杂)。那怎么办呢?我们可以用猜的,假设我人品好,猜几次就猜中了一条小于长度a的路径,我画画画画,好的,我得到了一条路径小于a的环路,问题解决了,皆大欢喜。可是,我不可能每次都猜的那么准,也许我要猜完所有种呢?所以我们说,这是一个NP类问题。也就是,我们能在多项式的时间内验证并得出问题的正确解,可是我们却不知道该问题是否存在一个多项式时间的算法,每次都能解决他(注意,这里是不知道,不是不存在)。

简而言之,NP类问题,没法在多项式时间内找到解,但是可以在多项式时间内验证解。

P类问题属于NP类问题。

什么是NP完全问题?
NP完全问题,即NPC问题(C是complete)

存在这样一个NP问题,所有的NP问题都可以约化成它。换句话说, 只要解决了这个问题,那么所有的NP问题都解决了。 其定义要满足2个条件:
首先,它得是一个NP问题;
然后,所有的NP问题都可以约化到它。

什么是NP难度问题?

NP-Hard问题是这样一种问题,它满足NPC问题定义的第二条但不一定要满足第一条(就是说,NP-Hard问题要比 NPC问题的范围广,NP-Hard问题没有限定属于NP),即所有的NP问题都能约化到它,但是他不一定是一个NP问题。
NP-Hard问题同样难以找到多项式的算法,但它不列入我们的研究范围,因为它不一定是NP问题。即使NPC问题发现了多项式级的算法,NP-Hard问题有可能仍然无法得到多项式级的算法。事实上,由于NP-Hard放宽了限定条件,它将有可能比所有的NPC问题的时间复杂度更高从而更难以解决。

引用来自这里

即,P类是在多项式时间内求解NP是在多项式时间内内验证NP完全是:是NP类同时可以约化到它;NP难问题是:所有NP可以约化到它,但不一定是NP问题

第2章 递归和分治策略

斐波那契数列

int Fibon1(int n)
    if (n == 1 || n == 2)
        return 1;
     else
        return Fibon1(n - 1) + Fibon1(n - 2);
    

汉诺塔

void hanoi(int n,int a,int b,int c)

	if(n>0)
	
		hanoi(n-1,a,c,b);
		move(a,b);//将a上的放到b
		hanoi(n-1,c,b,a);
	
  

分治法的三步及代码
分治法在每一层递归上都有三个步骤:

分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
合并:将各个子问题的解合并为原问题的解。

它的一般的算法设计模式如下:

Divide-and-Conquer(P)
1.  if |P|≤n0
2.    then return(ADHOC(P))//规模较小直接解决
3.  将P分解为较小的子问题 P1 ,P2 ,...,Pk
4.  for i←1 to k
5.    do yi ← Divide-and-Conquer(Pi)    △ 递归解决Pi//无法直接解决 所以要递归地解决
6.  T ← MERGE(y1,y2,...,yk)             △ 合并子问题//合并
7.  return(T)

其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC§是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时,直接用算法ADHOC§求解。算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。

来自这里

求时间复杂度的主定理
if(logb(a)!=d),T(n)=nmax(logb(a),d) ;
else T(n)=ndlogn;(即再乘一个logn)

其中b是除数,作为log的底。比较logba与n的系数d的大小,取大的。
如果一样大就取d,然后再乘个logn

二分搜索

int BinarySearch(int n)//在0到n-1之间搜索 

	int l=0,r=n-1;
	while(l<=r)
	
		int mid=(l+r)/2;
		if(x==a[mid]) return mid;//找到了
		else if(x<a[mid]) r=mid-1;
		else if(x>a[mid]) l=mid+1;
	
	return -1;//能运行到这里说明上面没有找到过 

每执行一次while算法,数组大小都会减小一半,故最多执行logn词。
最坏情况下的时间复杂性为 O(logn)

归并排序
时间复杂度O(nlogn)

void MergeSort(int l,int r)

	if(l<r)//至少有两个元素,不能重合 
	
		int mid=(l+r)/2;
		MergeSort(l,mid);//分解 
		MergeSort(mid+1,r);
		Merge(l,mid,r);//合并 
		Copy();//复制数组 
	
  

总代码模板:

void merge_sort(int q[],int l,int r)

	if(l>=r) return;
	
	int mid=(l+r)/2;
	
	merge_sort(q,l,mid);
	merge_sort(q,mid+1,r);
	
	int k=0,i=l,j=mid+1;//k是 结果数组里的个数
	while(i<=mid&&j<=r)
	
		if(q[i]<=q[j]) temp[k++]=q[i++];
		else temp[k++]=q[j++];
	  
	
	while(i<=mid) temp[k++]=q[i++];
	while(j<=r) temp[k++]=q[j++];
	
	for(i=l,j=0;i<=r;i++,j++) q[i]=temp[j];


快速排序
有中枢元素。
平均O(nlogn),最坏O(n2)(刚好逆序的时候)

void QuickSort(int l,int r)

	if(l<r)
	
		int mid=partition(l,r);//划分,其实就是核心的排序——找一个中枢,让左边的都小,右边的都大 
		QuickSort(l,mid);//递归对左半段排序 
		QuickSort(mid+1,r);
	
  
 
int partition(int l,int r)

	int i=l,j=r+1;//因为j要--j 先算后用,所以j要先+1
	int x=a[l];
	while(1)
	
		while(a[++i]<x&&i<r);//注意左指针要小于右指针 
		while(a[--j]>x);
		if(i>=j) break;
		swap(a[i],a[j]);//但凡走到这一步,说明a[i]>=x,a[j]<=x 该交换了 
	 
	a[l]=a[j];
	a[j]=x;//交换a[l]和a[j],把a[l]放在中间——它本来就是中枢元素 
	return j;//返回划分点的下标 

一些更好理解的其他模板:

void quick_sort(int q[],int l,int r)

	if(l>=r) return;//i j 相遇
	int x=q[l+r>>1],i=l-1,j=r+1; //确定x,中间数,ij开始要在范围外——这样一进去就能比较 
								//x在最左或最右的可能出现最坏情况 
	while(i<j)
	
		do i++;while(q[i]<x);
		do j--;while(q[j]>x);
		if(i<j) swap(q[i],q[j]);
	 
	
	quick_sort(q,l,j);//左右递归 可以写i,因为ij左右对称 
	quick_sort(q,j+1,r);


中位数问题
思想:将两个有序的数组合成一个有序的数组解决问题。
题目:给定两递增数组a1和a2,求两数组元素集合的中位数。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=100000+10;
int n;
int a[N],b[N];
int c[2*N]; 
int main()

	cin>>n;
	for(int i=0;i<n;i++)
	
		cin>>a[i];
	
	for(int i=0;i<n;i++)
	
		cin>>b[i];
	
	int k=0,i=0,j=0;
	while(i<n&&j<n)
	
		if(a[i]<=b[j]) c[k++]=a[i++];
		else c[k++]=b[j++];
	
	while(i<n) c[k++]=a[i++];
	while(j<n) c[k++]=b[j++];
	
	cout<<c[(k-1)/2];
	
	return 0;

第3章 动态规划

动态规划的步骤

  1. 分析最优解结构
  2. 建立递归关系
  3. 计算最优值

动态规划的基本要素

  1. 最优子结构
  2. 重叠子问题

动态规划法的变形——备忘录方法。

动态规划与分治法的区别
动态规划算法与分治法类似,其基本思想是将待求解问题分解成若干子问题,先求解子问题,再结合这些子问题的解得到原问题的解。与分治法不同的是,适合用动态规划法求解的问题经分解得到的子问题往往不是互相独立的。 若用分治法来解这类问题,则分解得到的子问题数目太多,以致最后解决原问题需要耗费指数级时间。
来自:宝哥的期末复习

矩阵连乘问题
可以看这里

最大子段和

int besti,bestj;//用来存答案ij下标
int n;//数字个数 数组是a[n] 
int MaxSum()

	int ans=0;//全为负数就为0 所以初始化为0 
	for(int i=1;i<=n;i++)
	
		int temp=0;
		for(int j=i;j<=n;j++)
		
			temp+=a[i];
			if(temp>ans)//比已经存的最大值要大 
			
				ans=temp;
				besti=i;
				bestj=j;
			
		
	

大概这样,可以求出所有的子端和。

01背包问题
一个AcWing上面优秀的解析

第4章 贪心算法

注意:局部最优未必总体最优。

会场安排问题
代码可以看看这里

贪心算法的两个基本要素

  1. 贪心选择性质
  2. 最优子结构性质

贪心选择性质
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。 问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。

贪心与背包和01背包
背包问题可以用贪心求解,01背包却不行。
因为它无法保证背包最终能装满,部分闲置的背包空间使单位背包空间价值降低了。

哈夫曼编码
是使用字符在文件中出现的频率表来建立一个用0,1串表示各字符的最优表示方式。
出现频率高的字符——较的编码。
出现频率低的字符——较的编码。
是文件的最优编码方案。


单元最短路径
一个模板bfs。

代码也许之后补充。

第5章 回溯法

回溯法有 “通用解题法” 之称。用它可以系统地搜索问题的所有解。回溯法是一个既带有系统性又带有跳跃性的搜索算法。
在包含问题的所有解的解空间树中,按照 深度优先搜索 的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。 (其实回溯法就是对隐式图的深度优先搜索算法) 。若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。 而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

回溯法的三个步骤

  1. 针对所给问题,定义问题的解空间
  2. 确定易于搜索的解空间结构
  3. 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索

01背包问题
回溯法的背包问题

第6章 分支限界法

求解目标:与回溯法不同。回溯法的求解目标是找出解空间中满足约束条件的所有解。 分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解
搜索方式: 以广度优先或以最小耗费优先 的方式搜索解空间树。分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。
此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。

其他

同学的复习:点这里

以上是关于算法设计与分析期末抱佛脚复习的主要内容,如果未能解决你的问题,请参考以下文章

概率论与数理统计期末复习抱佛脚:公式总结与简单例题(完结)

算法设计与分析期中考试复习:代码和经典题目 分治二分动态规划(未完待续)

USTC算法设计与分析-总结

USTC算法设计与分析-总结

USTC算法设计与分析-总结

计算机算法 期末复习个人笔记(部分)