《算法分析与设计》笔记整理

一:分治法

(1)全排列问题

问题:打印出给定的n个字符的全排列。
思想:递归,将问题转化为前缀+n-1个字符的全排列
Notations:
list[]:一定顺序的n个字符字符串
k:当前前缀设置位置k号
m:字符总数量

void Perm(list[],int k,int m)
{
   
	if(k ==m)
	{
   
	  for(int i=0;i<m;i++)
	  cout<<list[i]<<' ';
	  cout<<endl;
	}
	for(int i=k;i<m;i++)
	{
   
	  swap(list[k],list[i]);  //设置k号确定字符
	  /*如何设置前缀?
	  从0号位置向后设置,可用字符为当前位置与后面位置字符
	  */
	  Perm(list,k+1,m);
	  swap(list[k],list[i]);  //还原k号字符与串初始顺序 循环设置新字符
	}
}

时间复杂度 T(n)=n(c1+c2+T(n-1))=nT(n-1)+(c1+c2)n

(1.2)Hanoi塔问题

问题:a,b,c三个塔座,开始时,在塔座a上有一叠共N个的圆盘,自下而上从大到小叠放在一起,编号1,2,…,n。现要求将a上的一叠圆盘移到塔座b。
思想:将问题转化为将上面n-1个盘子移到c,把n号盘子移到b。再将c号的n-1个盘子按照相同的方法移到b。
Notations:
n:num.盘子
a,b,c:三个塔座

void Hanoi(int n,int a,int b,int c)
{
   
 	if(n >0){
   
 	Hanoi(n-1,a,c,b);  //将1-n-1号盘子从a-->c
 	Move(a,b); 
 	Hanoi(n-1,c,b,a);  //将1-n号盘子从c-->b
}

时间复杂度:T(n)=2T(n-1)+C=2(2T(n-2)+C)+C=O(2^n)

(2)整数划分问题

问题:将正整数n表示成一系列正整数之和,n=n1+n2+…+n_k(n1>=n2>=n3…>=nk)。这种表示称为正整数n的划分。正整数n的不同划分个数称为正整数n的划分数,记为p(n)。例如p(6)=11

6=5+1=4+2=4+1+1=3+3=3+2+1=3+1+1+1=2+2+2=2+2+1+1=2+1+1+1+1=1+1+1+1+1+1
思想:设q(n,m)为数字m对n的拆分数。当n=1,m=1时,q(1,1)=1。当n=m时,q(n,m)=q(n,m-1)+1。当n>m时,q(n,m)=q(n,m-1)+q(n-m,m)即下一拆分项与拆分后的新项。当n<m时,q(n,m)=q(n,n)。

代码

int q(int n,int m)
{
   
 	if((n==1)&&(m==1)) return 1;
 	else if(n >m) return q(n,m-1)+q(n-m,m);
 	else if(n ==m) return q(n,m-1)+1;
 	else return q(n,n);
}

(3)大整数乘法问题

问题描述:设X和Y都是n位二进整数,现在要计算它们的乘积,设计效率较高的方案。
在这里插入图片描述

(4)棋盘覆盖问题

问题描述:
在这里插入图片描述
输入:n*n棋盘大小,x,y特殊点大小
输出:棋盘覆盖格式
约束条件:只能使用4种L进行摆放填充
思路:将2^k * 2^k的棋盘分割为 4个2^k-1 * 2^k-1子棋盘,特殊方格必位于4个棋盘之一。用一个L型方格覆盖其他三个棋盘,从而将原问题转化为4个较小棋盘的棋盘覆盖问题
在这里插入图片描述
Notations:
tr:左上角方格行数
tc:左上角方格列数
dr:特殊点所在行
dc:特殊点所在列

/*
同思路中的L型填充不同,实际的代码过程是从左至右依次处理较小方格。
*/
void cheessBord(int tr,int tc,int dr,int dc,int size)
{
   
	if(size ==1) return;
	int t =title++;
	size /=2;
	if((dr<tr+s)&&(dc<tc+s)) //特殊点左上角
		chessBord(tr,tc,dr,dc,size);  //进入递归
	else  {
    //特殊点不在左上角 则L覆盖后再递归
		borad[tr+s-1][tc+s-1] =t;
		chessBord(tr,tc,tr+s-1,tc+s-1);
	}
	else if((dr<tr+s)&&(dc>=tc+s))  //特殊点在右上角
		chessBord(tr,tc+s,dr,dc,size); //进入递归
	else {
    //特殊点不在右上角 L覆盖后在递归
		board[tr+s-1][tc+s] =t;
		chessBord(tr,tc+s,tr+s-1,tr+s,size);
	}
	else if((dr>=tr+s)&&(dc<tc+s))  //特殊点在左下角
		chessBord(tr+s,tc,dr,dc,size); //进入递归
	else {
    //特殊点不在左下角 L覆盖后在递归
		board[tr+s][tc+s-1] =t;
		chessBord(tr+s,tc,tr+s,tr+s,size);
	}
	else if((dr>=tr+s)&&(dc>=tc+s))  //特殊点在右下角
		chessBord(tr+s,tc+s,dr,dc,size); //进入递归
	else {
    //特殊点不在右下角 L覆盖后在递归
		board[tr+s][tc+s] =t;
		chessBord(tr+s,tc+s,tr+s,tr+s,size);
	}
}

(5)合并排序

基本思想:将待排序的元素分成大小大致相同的两个子集合,分别对两个子集合进行排序。最终将排好序的子集合合并成要求的排序好的集合
在这里插入图片描述

void mergeSort(vector<int> &vec,int left,int right)
{
   
	int mid =(left+right)/2;
	mergeSort(vec,left,mid);      //左集合
	mergeSort(vec,mid+1,right);   //右集合
	merge(vec,left,mid,right);
}
void merge(vector<int>& vec,int left,int mid,int right)
{
   
	vector<int> temp;  //临时保存当前排好序的元素
	int i =left,j =mid+1;
	while((i<=mid)&&(j<=right)) {
   
		if(vec[i] <vec[j]) 
		temp.push_back(vec[i++]);
		else temp.push_back(vec[j++]);
	}  //按照升序将左右子集元素放在临时向量vec
	while(i<=mid) temp.push_back(vec[i++]);
	while(j<=right) temp.push_back(vec[j++]); //不等长元素加入temp
	for(int k=0;k<temp.size();k++)
		vec[k+left] =temp[k];   //temp[k]-->vec[left+k]实际位置修改
}

时间复杂度:
在这里插入图片描述

(6)快速排序

基本思想:对于输入的子数组a[p:r],按以下三个步骤进行排序
①分解:以a[p]为基准元素将a[p:r]划分成3段a[p:q-1],a[q],a[q+1:r],使a[p:q-1]中任意一个元素小于a[q],而a[q+1:r]中任意一个元素大于a[q]
②递归求解:分别对a[p:q-1]和q[q+1:r]进行排序
③合并:a[p:q-1],a[q+1:r]排好序后,a[p:r]已经排好序
在这里插入图片描述

Model:

void quickSort(int a[],int p,int r)
{
   
	if(p <r) {
   
		int j =Partition(a,p,r);  //思想中的划分q
		quickSort(a,p,j-1);
		quickSort(a,j+1,r);
	}
}
int Partition(int a[].int p,int r)
{
   
	int x =a[p];   //a[p]作为基准值
	int i =p,j= r+1;
	while(1) {
   
		while((a[++i]<x)&&(i <r)); //左哨兵找大于x的点
		while(a[--j]>x);  //右哨兵找小于x的点
//找到各自点后 判断i<j? 若不成立则证明左右子数组已满足性质 退出循环
		swap(a[i],a[j]);		
	}
	a[p] =a[j];    //!!!
	a[j] =x;
	return j;
}

上述代码是以左侧第一个元素作为基准值,而快速排序算法的性能取决于划分的对称性,因此可以使用随机选取基准点的方式,减少最坏情况的可能

int randomPartition(int a[],int p,int r)
{
   
	int i =rand()/(double)RAND_MAX *((right-left)+left);
	swap(a[i],a[p]);
	return Partition(a,p,r);
}

时间复杂度:
最好时间复杂度:O(nlogn)
最差时间复杂度:O(n^2)

(7)循环赛日程表

问题描述:
设计一个满足以下条件的比赛日程表:
(1)每个选手必须与其他n-1个选手各赛一次
(2)每个选手一天只能赛一次
(3)循环赛一共进行n-1天
思路:将选手分为两半,n个选手的日程表可以通过过两组n/2个选手的日程表确定。递归使用这种一分为二的策略对选手分割。当仅剩下2个选手时,就容易安排比赛
步骤:
(1)初始化第一行
1 2 3 4 n=4
(2)使用第一行交叉填充第二行
1 2 3 4
2 1 4 3 {第一部分n=2}
(3)使用第一部分填充第二部分
1 2 3 4
2 1 4 3
3 4 1 2
4 3 2 1 {第二部分n=1}
在这里插入图片描述

(8)最接近点对问题

一维情形:
在这里插入图片描述
(1)思路
①基于平衡子问题思想,选取中位数作为分割点,将S划分为S1,S2两个子集
②递归在S1,S2求解得到最接近点对(p1,q1),(p2,q2),其中p3∈S1,q3∈S2的点对(p3,q3)也可能构成最接近点对
③p3,q3分别是S1,S2的最大和最小值,可以在线性时间内查找得到两值

二维情形:
在这里插入图片描述
输入:二维平面上n个点的集合
输出:距离最近的两个点
(1)思路:
①选取x=m,m为所有点x坐标的中位数把S分割为S1和S2
②递归的在S1,S2找到最接近点对(p1,q1),(p2,q2),其中p3∈S1,q3∈S2的点对(p3,q3)也可能构成最接近点对,如何在线性时间查找p3,q3呢?
在这里插入图片描述

二:动态规划

(8)0-1背包问题

问题描述:给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中的物品总价值最大。
输入:C>0 wi>0 vi>0 1<=i<=n
输出:(x1,x2,…,xn) (xi=0 or 1)
约束条件:ΣwixI <=C Max{ Σvixi }
①求什么设什么:从物品i到物品n选择装入容量为j的背包,背包内物品的总价值t(i,j)。
②分析证明重叠子问题:从物品i到物品n选择装入容量为j的背包。当wi>j时,总价值与t(i-1,j)相同。当wi<=j时,t(i,j)应该为选择i物品后选择(i+1)~ n物品装入容量为j-wi的包与选择i+1~n装入容量为j的包的最大值。从上到下分析,容易得出存在重叠子问题。
③证明最优子结构:假设t(i,j)是容量为j选择物品为i,i+1…n的最优解。其子问题①wi>j时 容量为j选择物品为i+1,i+2…n ②wi<=j时 容量为j-wi选择物品为i+1…n 与 容量为j选择物品为i+1,i+2…n。假设子问题非最优解,则①时最优解应该大于t(i+1,j) ②时最优解应该大于t(i+1,j-wi)。此时i~n个物品的最优解>t(i,j),结果矛盾。
④递推公式:
t(i,j) =t(i+1,j) wi>j
=t(i+1,j-wi)+vi wi<=j
在这里插入图片描述
Note: 下面所述代码中,v[n+1],w[n+1] 第一位均存0.
m[n+1][c+1]的第一排(0)与第一列全0

void Knapsack(int v[],int w[],int c,int n,int **m)
{
   
//自底向上运算,【1】仅一个物品时:若j<wi时填充为0
   int jmax =min(c,w[n]-1);
   for(int i=0;i<=jmax;i++)
   m[n][i] =0;
//【1】若j>wi时 填充此物品价值
   for(int i=w[n];i<=c;i++)
   m[n][i] =v[n];
//【2】递推公式 自底向上运算
   for(int i=n-1;i>1;i--) {
    //第二排(1)仅考虑m(1,n)
   jmax =min(c,w[i]-1);
   //1.wi >j时
   for(int j=0;j<=jmax;j++)
   m[i][j] =m[i+1][j];
   //2.wi <j时
   for(int j=w[i];j<=c;j++)
   m[i][j] =max(m[i+1][j],m[i+1][j-w[i] ]+v[i]);
   }
//【3】单独算m(1,c)
   m[1][c] =m[2][c]; //初始化
   if(c >=w[1])
   m[1][c] =max(m[1][c],m[2][c-w[1] ]+v[1]);
}
//最优解回溯算法Traceback()
void Traceback(int **m,int w[],int c,int n,int x)
{
   
   for(int i=1;i<n;i++) {
   
   if(m[i][c] ==m[i+1][c]) 
   x[i] =0;
   else {
   
   x[i] =1;
   c -=w[i];
   }
   }
   if(m[n][c]) x[n] =1;
   else x[n] =0;
}

时间复杂度:
Knapsack(): T(n)=O(m*n) m为最大容量
Traceback(): T(n)=O(n)

(9)背包问题

问题描述:设有n个物体和一个背包,物体i的重量为wi,价值为vi,背包的容量为c。若将物体i的xi部分(1<=i<=n,0<=xi<=1)装入背包。则具有价值为vi,xi。目标是找到一个方案。使放入背包的物体总价值最高。
输入:c >0, n >0, wi >0, vi >0
输出:{x1,x2,…,xn} (0<=xi<=1)
约束条件:ΣwixI <=C Max{ Σvixi }
①分析最优策略:若把物品按照总价值从大到小装入背包,可能因为重量过高而出错。若把物品按重量从小到大装入背包,若价值较少也可能出错。若把物体按照单位体积从大到小装入背包,好像符合题意。
②证明最优子结构性质(反证法):设A使容量为C的最大价值物品的集合。若j是集合A中的物品,从A中取出j,得到A’是容量为C的物品的集合。假设B’是去除j物品的最大价值物品集合,且|B’|>|A’|. B =B’+j |B|>|A|矛盾。
③证明最优策略(归纳法):
在这里插入图片描述

Note:
vector< int >value,weight;
vector< float >x;
value:物品i的全职(n+1)
weight:物品i的重量(n+1)
x:物品i装入背包的比例(n+1)

bool cmp(int left,int right)
{
   //计算单位重量价值 并按照升序排列
   l =value[left]*1.0/weight[left];
   r =value[right]*1.0/weight[right];
   return l>r;
}
//value weight x为全局变量且在主函数中通过resize(n+1)进行了初始化
void Knapsack(int n,int c)
{
   
//[1]对物品按照单位重量价值排序
   sort(weight.begin()+1,weight.end(),cmp);
//weight[0]为无意义元素 不需要进行排序
//[2]贪心策略  循环装入背包
   int i =0;
   for(i=1;i<=n;i++)
   {
   
//weight按照单位重量价值按降序排序,最优解是将尽可能多的单位价值高的物品加入背包
       if(weight[i] >c)
       break;
//当较高单位重量价值的物品无法完全放入时 退出循环 并加入该物品的部分于背包
       x[i] =1;
       c -=weight[i];
   }
   if(i <=n)
   x[i] =c*1.0/weight[i];
}

(10)最大子段和

问题描述:给定长度为n的整数序列a[1…n],求[1,n]某个子区间[i,j]使得a[i]+…a[j]和最大。
输入:a[n]长度为n的序列
输出:最大子段和b[j]
约束条件:给定曲间某最大的字段和
思路:(1)枚举法 求给定序列的最大字段和可以枚举曲间内所有子区间和并取最大值即得解。但枚举法得时间复杂度为O(n^2) (2)分治法:将序列分为左右两个子段,最大子段和则存在三种情况 1:在左子序列 2:在右子序列 3:开始侧在左子序列,结束侧在右子序列。 1 2两种情况可以递归考虑。对于情况3:a[n/2]与a[n/2+1]一定在3序列中,只需求得[1,a[n/2]]得最大字段和s1和[a[n/2+1],n]得最大子段和s2。s=s1+s2即为第三种情况下的最大子段和。此时时间复杂度为O(nlogn) (3)动态规划算法:使用第二种算法考虑中间状态时,每层递归中都包含有重复计算的重叠子问题(重叠子问题性质),同时设b[j]是以j结尾得子段得最大字段和。则当b[j]>0时,b[j+1]=b[j]+a[j]。
(0)求什么,设什么: 设以第j个元素结尾得最大子段和为b[j]。分析可知b[j]与b[j-1]相关。
(1)重叠子问题性质:设b[j]=max{a[i]+a[i+1]+…a[j]} b[j-1]=max{a[i]+a[i+1]+…a[j-1]}。上述问题描述中存在重叠得计算过程。
(2)最优子结构性质:设b[j]是以j结尾得最大子段和,当b[j-1]>0时,b[j]=b[j-1]+a[j]。设B’是以j-1结尾得最大子段和(B’>b[j-1]),则B=B’+a[j]>b[j]矛盾。
(3)递推公式:
b[j] =b[j-1]+a[j] (b[j-1]>0)
b[j] =a[j] (b[j-1]<=0)

在这里插入图片描述
Notations:
sum:子段和
a[n+1]:n个元素构成的序列

int sumMax(int n,int a[])
{
   
	int sum =0;
	for(int i=1;i<=n;i++) {
   
	if(sum <0) 
		sum =a[i];   //不会更小  s[i]=a[i]
	else {
   
	sum =sum+a[i];  //正增长 s[i]=,s[i-1]+a[i]
	}
	return sum;
}

(11)最长公共子序列

问题描述:给定2个序列X={x1,x2…,xm},Y={y1,y2…,yn}。找出X和Y的最长公共子序列
输入:X={x1,x2…,xm},Y={y1,y2…,yn}
输出:Z =X和Y的最长公共子序列
约束:
思路:Xi={x1,x2…,xi}(i=1,2,…,m),Yj={y1,y2,…,yj}(j=1,2,…,n) 分别是X,Y以xi和yj结尾的连续子段。分别求得Xi在j=(1,2,…,n)所有子段中的公共子序列长度。当xi=yj时,字段长度=1+Xi-1和Yj-1的公共子序列长度。当xi!=yj时,则需继承之前存在的公共子序列长度即Xi和Yj-1或Xi-1和Yj。
①求什么,设什么:求X=m和Y=n的序列的公共子序列长度,则设t[i][j]是长度为i,j的序列X’,Y’的公共子序列长度。
②重叠子问题性质:根据思路分析求解t[i][j]可能会用到t[i-1][j-1]、t[i][j-1]、t[i-1][j],满足重叠子问题性质。
③最优子结构:将问题转换成如上子问题,即当x[i]=Y[j]时 t[i][j]=t[i-1][j-1]+1 否则t[i][j]=max(t[i][j-1],t[i-1][j])。设t[i][j]是X,Y的最优解,若Xi-1Yj-1的最优解为T’>t[i-1][j-1]则T=T’+1>t[i][j] 矛盾。
④递推公式:
t[i][j] =t[i-1][j-1]+1 X[i]=Y[i]
=max(t[i-1][j],t[i][j-1)
在这里插入图片描述

Notations:
X,Y:求解的两个序列
t[i][j]:以X[i]和Y[j]结尾的X’,Y’子序列的最长公共子序列长度

 void LCSlength(string x,string y,vector<vector<int> >&a,vector<vector<int> >&b)
{
   
	//初始化第一行和第一列为0
	int n =x.size(),m =y.size();
	for(int i=0;i<=n;i++) a[i][0] =0;
	for(int j=0;j<=m;j++) a[0][j] =0;
/*Note:递推公式 自底向上运算。 字符串的索引从0~size-1,因此数组索引时需要些许改动 不能直接1对1填写   x的第i个字符和y的第j对应a[i+1][j+1]
*/
	for(int i=1;i<n;i++) {
   
	for(int j=1;j<m;j++) {
   
	//x[i]=y[j]情况下 
	if(x[i] ==y[j]) {
   
		a[i+1][j+1] =a[i][j]+1;
		b[i+1][j+1] =1;    //斜上方
	}
	else {
   
		if(a[i][j+1] >a[i+1][j]) {
   
			a[i+1][j+1] =a[i][j+1];
			b[i+1][j+1] =2;  //上侧
		}
		else {
   
			a[i+1][j+1] =a[i+1][j];
			b[i+1][j+1] =3;  //左侧
		}
	}
}
void LCS(vector<vector<int> >&b,string x,int i,int j)
{
   
	if(b[i][j] ==1) {
   
	LCS(b,x,i-1,j-1);
	cout<<x[i];
	}
	else if(b[i][j] ==2)
	LCS(b,x,i-1,j);
	else if(b[i][j] ==3)
	LCS(b,x,i,j-1);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冠long馨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值