这里写的是"MOOC程序设计与算法"----郭灿的笔记
一、枚举
1.2生理周期
人有体力,情商,智商的高峰期,分别每隔23天,28天,33天出现一次,我们想知道每个人何时三个高峰落在同一天。
给定三个高峰出现的日子abc,给定任意一天d,输出距离下一次三个高峰落在同一天还有多久。
保证输出天数小于21252。
解题思路:
对于d+1~21252之间的每一天k,只需要判断是否同时是体力情商智商的高峰期即可,即判断k和abc的差是否同时分别为23,28,33的整数倍,即判断下式是否为真:
(k-a)%23==0&&(k-b)%28==0&&(k-c)%33==0
但问题是如何枚举的更快一些?
“跳着枚举”,在第一次找到体力的高峰期和下一次体力的高峰期
之间会有很多不需要判断的日子,所以我们可以每次加23天,省去中间的日子,同理,在第一次找到体力和情商高峰期同时出现的一天之后,就可以跳过中间23和28的最小公倍数天,直到找到三个高峰同时出现的一天。
1.3称硬币
现在有11枚真币和1枚假币,不知道假币是重是轻,现有一个天平,给出三次测量结果,试给出哪一枚硬币是假币,是重还是轻,题目保证能得出结果。
解题思路:
用枚举的方法,我们分别假设第一到第十二枚硬币为假币,是重或者轻,然后再代入给出的结果进行检验,如果三次检验后的结果都通过就说明找到了假币,思路还是比较容易的。
补充:
找出假币有更普遍的规律,即讨论最少称几次能从n枚硬币中找到假币,见洛谷P1431 找出伪币。
1.4熄灯问题
原题描述复杂,见POJ1222
解题思路:
拿到题第一思路应该就是枚举,一共有5x6个开关,每个开关对应0和1,如果是直接来的话,就有种方式,太多了会超时。我们就需要思考如何减少枚举的数目。
基本思路:找到某个“局部”,当局部的状态确立之后,局部以外的状态只能是确定的一种或者不多的n种,这样就减少了枚举的总数,只需要枚举这局部的状态就可以。
对于本题来说,存在着这样的“局部”----第一行的开关状态,假设第一行的开关状态被确立之后,是不是第二行就紧跟着被确立了?因为我们想要达到30个开关全部关闭的状态,那么第一行开着的灯只能由第二行关闭,所以第一行的开关状态就是我们需要的“局部”,我们只需要枚举第一行的全部状态,检查最后是否全部关闭即可。
二、递归
提到递归,我们就应该思考两点,递归终点和递归表达式,尽可能的找到题目要求的f(n)和f(n-1)甚至与f(n-2)的关系。
2.2汉罗塔Hanoi
提到递归就有著名的汉罗塔问题,这里简述一下问题,有个从大到小的圆盘摆
放在
柱上(大在下),要把这
个圆盘放在
柱上,中途可以借用
柱,要求不得出现大盘在小盘上面。
解题思路:
我们要有整体和局部的思想,也要考虑极端情况下。
题目说了有个圆盘在
柱上,我们先思考
的时候,只需要直接把这个圆盘从
就可以了
再来考虑的情况,这时候就要想到整体和局部,要把这
个圆盘移动到
柱上可以简化成以下3个步骤
1、把最上面个圆盘移动到
柱。
2、再把剩下第个圆盘移动到
柱上。
3、最后把柱上的圆盘移动到
柱上。
但是我们需要考虑的是能否把这n-1个圆盘看成整体来移动?
答案是能的,考虑递归函数,在处理这个圆盘的时候,再把第n-1个圆盘单独出来,把上面
个圆盘看成整体,按同样的移动方式递归下去。
2.3N皇后问题
这里使用的应该是回溯算法来解决该问题
解题思路:要让这N个皇后放到一个NxN的棋盘上且不互相攻击,则每一行有且仅有一个皇后,针对这个分布情况,我们不必再用一个二维数组来记录已经摆放好的皇后位置而是只需要用一个一维数组Q[max]就够了,Q[i]=j就表示了第i行第j列存在了一个皇后
个人补充:这样的记忆坐标的方式也可以推广至一行存在多个“皇后”的情况,比如当最多有100x100的规模时,就用一个整数的不同位数来记录坐标,比如q[i]=001045032表示第i行第1、45、32列分别存在着一个“皇后”,不过这样的储存方式终究有上限罢了。
解决了储存已放置皇后的问题后便是如何放置的问题。
首先是解释回溯算法,回溯算法也就是一个类似枚举的搜索过程,当遇到不满足条件的情况下就返回尝试其他路径,这种走不通就退回再走的技术为回溯法。其中遇到的不满足条件的情况称为回溯点。
对于N皇后问题而言,回溯点便是被其他已放置的皇后攻击,即在该行该列和45°、135°对角线上有其他皇后存在我们用函数int isSafe(int i,int j)来判断第i行j列是否可以摆放皇后。为了方便讲解递归,这里附带代码(伪):
define N 100 //皇后的数量
int q[N]; //记录各行皇后所在的列
int count=0; //统计N皇后问题解的个数
int isSafe(int i,int j)
{
可以摆放return 1;
不可摆放return 0;
}
void NQueen(int k) //假设0~k-1行的皇后已经被摆好后,摆放第k行及其以后的皇后
{
int i;
if(k==N) //N个皇后已经摆完以后就开始输出每一行皇后的位置
{
count++;
for(i=0;i<N;i++)
printf("%d ",q[i]);
printf("\n");
return;
}
for(i=0;i<N;i++) //寻找第k行皇后的位置
{
if(isSafe(k,i)) //实际上这里不用函数可以省去多余的判断提高效率(及时的break)
{
q[N]=i;
NQueen(k+1);
}
else break;
}
}
那么为什么NQueen(0)可以输出所有的情况呢,归根结底是因为第k行的每个位置都去判断过,这里的递归是从前往后递归,跟之前的从后往前递归不同。
时间复杂度的分析:
可以看见回溯算法的效率永远是最低的,单讨论每一行的皇后不考虑对角线的皇后攻击的话,第一行有N个位置,第二行有N-1个位置,那么时间复杂度应该是O(N!)。
2.4逆波兰表达式
要求计算出前缀表达式的运算结果。
解题思路:
这个题本身就是递归形式的题,逆波兰表达式的定义如下:
1,一个数是一个逆波兰表达式,值为其本身
2,“运算符号 逆波兰表达式A 逆波兰表达式B”是一个逆波兰表达式,值为A和B经过运算后的结果。
题目要求我们读入一个逆波兰表达式并计算其值,我们一个一个来看,先读入的一个值,有可能是数字也有可能是加减乘除运算符,如果读到数字,我们只需要返回其本身的值即可,如果是运算符,那么其后应该还需两个“逆波兰表达式”与之构成一个“逆波兰表达式”,所以如下:
定义:f()中包含了输入的表达式,即函数里面就有要求输入的语句。
当输入“number”的时候,return number(这个值本身);
当输入“+”的时候,return f()+f();
当输入“-”的时候,return f()-f();
......。
3.2上台阶
树老师爬楼梯,每次可以走一级或者两级,输入楼梯级数N,求不同的走法。。
解题思路:
先从一般的情况来找规律,假设楼梯有n级,设n级楼梯有 种走法
...
由于是讲顺序的,那么对于这n个阶梯,我们只需要考虑第一次怎么走,走一级或是走两级,都是不同的走法所以可以找出递归的表达式如上。这是比较标准的递归函数,先找边界条件再找递归关系式。
边界条件:使递归函数停止递归调用的终点。
int upstairs(int n)
{
if (n == 1) return 1;
else if (n == 2)return 2;
else return upstairs(n - 1) + upstairs(n - 2);
}
3.3放苹果
把M个苹果放在N个同样的盘子里,允许有的盘子空着,问有多少种方法,注:511和151是同一种方法。 ,
(添加一个问题,计算要求盘子不空的状况下有多少种方法)
解题思路:
我个人有一个枚举的思路:把M拆成k个数之和,要求k≤N,累计有多少种拆法即可。
跟上一个有点类似但不全相同,同样设M个苹果放在N个盘子里有 种方法。
边界值:
情况一:没有苹果,那么;
情况二:没有盘子,那么;
情况三:M≤N,那么,也就等于整数M的拆分方案(有兴趣可以做);
寻找递归表达式:
在递归中分类是很重要的思想。
当M>N时候,我们对苹果的放法进行讨论来使得M或者N能够逐渐减小;
情况一:有盘子为空,也就是说至少存在一个盘子为空,我们就把它丢掉。
情况二:没有盘子为空,也就是说每个盘子都至少有一个苹果,我们就把这N个苹果丢掉
综上可以知道,
所以可以得出如下的递归函数:
int placeApple(int M, int N)
{
if (M == 0)return 1;
if (N == 0) return 0;
if (M < N)return placeApple(M, M);
return placeApple(M, N - 1) + placeApple(M - N, N);
}
需要注意的有,M=0的时候算是一种放法--往盘子里放0个苹果,而不能用M=1来终止循环,比如当M=N的时候,下一次递归的函数是(0,N)就死循环了。要避免这一点,可以看讨论的时候提到的整数M的拆分方案,当M=N的时候,返回的是一个固定的数。
3.4算24
这个题相信大家小学初中都玩过吧。
给出4个小于10的正整数,判断有没有一种解,使得结果为24,要求只能使用四则运算和括号且必须用完这4个数。(注意,这里的除法是实数运算除法,也就是实际生活中的运算)
解题思路:
我们本着遇到问题思考如何分解成子问题的思想,4个数运算得出24,我们如何拆成子问题,也许这些子问题的形式就跟原问题相似只不过规模更小,也就是存在递归关系。
那么我们第一步要干什么?计算两个数。
对于n个数,我们第一步就是先拿两个数出来运算,再和这剩下的n-2个数去继续运算求解24,也就变成了n-1个数求解24的问题了,可以看出,这个规模从n→n-1。
那么取哪两个数,作何种运算呢,这并无要求,故我们要枚举先算的两个数和这两个数的运算方法
#define EPS 1e-10 //浮点数的比较是两者差的绝对值小于精度EPS
double ans = 0, num_a[5];
int type = 0;
void count24(int n,double num[]) //n个数计算24
{
if (type)return;
if (abs(num[0] - 24) < EPS && n == 1) //边界值:一个数的时候,且等于24
{
type = 1;
printf("YES\n");
return;
}
else if (abs(num[0] - 24) > EPS && n == 1)
return;
double num_b[5]={0};
int i,j;
for (i = 0; i < n-1;i++)
for (j = i+1; j < n;j++) //枚举出来的两个数num[i]和num[j]运算后得到newnum再和剩下n-2个数存入新数组num_b
{
int m, k;
for (k = 0,m=0; k < n; k++)
if (k != i && k != j)
{
num_b[m] = num[k];
m++;
}
num_b[m] = num[i] + num[j];
count24(n-1, num_b);
num_b[m] = num[i] * num[j];
count24(n-1, num_b);
num_b[m] = num[i] / num[j];
count24(n-1, num_b);
num_b[m] = num[j] / num[i];
count24(n-1, num_b);
num_b[m] = num[i] - num[j];
count24(n-1, num_b);
num_b[m] = num[j] - num[i];
count24(n-1, num_b);
}
}
小插曲(1):动规解题的一般思路
1,将原问题分解为子问题
将问题分解为若干个子问题,这些子问题和原问题形式相同或类似,只不过是规模变小了而已,解决完子问题,原问题也就解决完了。
子问题的解一旦求出便会保存起来,这样所有的子问题只会计算一遍。
2,确定状态
在用动规解题的时候,我们将和子问题相关的一系列变量的一组值,称为一个“状态”,一个状态对应一个或多个子问题,所谓这个“状态”的“值”,就是该“状态”对应的“子问题”的“解”。
所有''状态''的集合构成了问题的''状态空间''的大小,与用动规解题的时间复杂度。 整个问题的时间复杂度=状态数目乘以计算单个状态所需的时间,且在每个状态上作计算花的时间都是和数据规模无关的常数
用动规解题,经常碰到的情况是,K个整型变量能构成一个状态,如果这K个整型变量的取值范围分别是N1,N2,·····Nk,那么,我们就可以用一个k维的数组array[N1][N2]·····[Nk]来存储各个状状态的''值''。这个''值''未必就是一个数字,可能是需要一结构体才能表示的,一个''状态''下的''值''通常会是一个或多个子问题的解。
3,确定一些初始状态(边界状态)的值
不同的问题,初始状态(边界状态)的值不同。
4,确定状态转移方程
定义了什么是''状态'',以及该''状态''下的''值''后,就要找出不同状态之间如何迁移,即:如何从一个或多个''值''已知的''状态''求出另一个''状态''的值,(''人人为我''递推型),状态的迁移可以用递推公式表示,称为''状态转移方程''。
小插曲(2):能够用动规解决的问题的特点
1) 问题具有最优子结构性质
如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。
2) 无后效性
当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取何种手段或经过哪条路径演变到当前的这若干个状态无关。
6.1数字三角形(递归)
在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上经过的数字之和最大,路径上每一步都只能往左下或者右下走,仅需求出这个最大和j即可,三角形的行数大于1小于等于100,数字为0-99,给出的数字第一行表示三角形行数,后面为三角形内容。
解题思路:
我们用一个二维数组就可以存储这些数据。
这是一个很明显的用递归去做的题,我们假设MaxSum(i,j)表示从第i行第j列到底边各条路径中的最大值,那么问题就变成了求解MaxSum(1,1)=?,从数字num(i,j)
MaxSum(i,j)=max{MaxSum(i+1,j)+MaxSum(i+1,j+1)}+num(i,j)
#include<stdio.h>
int p[101][101]={0};
int n;
int MaxSum(int l, int r)
{
if (l == n) return p[l][r];
int A = MaxSum(l + 1, r );
int B = MaxSum(l + 1, r + 1);
return (A > B ? A : B) + p[l][r];
}
int main()
{
int i,j;
scanf_s("%d", &n);
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
scanf_s("%d", &p[i][j]);
printf("%d", MaxSum(1,1));
return 0;
}
但是这样是过不了的,因为会TLE(超时)
这是由于我们重复计算了大量的MaxSum,使时间复杂度到了,如果我们可以把计算过的Maxsum储存起来,下一次再遇到相同的Maxsum的时候直接使用值就可以把时间复杂度压缩到
级别,提快了很多。
#include<stdio.h>
int p[101][101];
int maxsum[101][101];
int n;
int MaxSum(int l, int r)
{
//多加一个判断是否是已计算过的值的if就可以避免很多重复运算
if(maxsum[l][r]!=-1) return maxsum[l][r];
if (l == n) return p[l][r];
int A = MaxSum(l + 1, r );
int B = MaxSum(l + 1, r + 1);
//把已经计算过的值存储起来
maxSum[l][r]=(A > B ? A : B) + p[l][r];
return maxSum[l][r]
}
int main()
{
int i,j;
scanf_s("%d", &n);
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
{
scanf_s("%d", &p[i][j]);
maxsum[i][j]=-1;
}
printf("%d", MaxSum(1,1));
return 0;
}
以该题为例,分析一下动态规划解题的特点和步骤:
特点:
对于数字三角形这个问题来说,原问题的最优解所包含的子问题的最大值也是有最优解的,而且当前子问题的值确定以后,往后的子问题的值的确定就只能当前子问题的值有关,与i和j是什么,是怎么到达i和j无关,以第一行第一列数字7为例,子问题有两个分别是MaxSum(2,1)和MaxSum(2,2),当这两个子问题的值确定以后,MaxSum(1,1)的值就只跟这两个值有关,也就是取最大值,与子问题的状态、子问题求得最优解的路径无关了,所以我们才可以用这种递归的方式去解决数字三角形这个问题。
1),分解子问题:
原问题就是MaxSum(i,j),第i行第j个数字到底边和的最大值,那么就拆分成子问题该数字正下方和其右下方的数字到底边和的最大值,这两个子问题如果解决了,那么原问题也就解决了。
2),确定状态:
''状态''就是子问题MaxSum(i,j)的数字所在行列i和j,''状态''所对应的''值''就是该数字到底边和的最大值。每个''状态''的''值''只需要计算一次,因为每一个状态对应的变量有i和j,那么我们就需要一个二维数组来存储每个状态的值。
3),确定初始状态:
''初始状态''就是底边的数字,''值''就是底边数字的值,从底边开始,推出上面的''子问题''的''状态''的''值''。
4),确定状态转移方程:
6.1数字三角形(递推)
递归函数都可以转化成递推,这道题也不例外,我们以给出的数据为例讲解递推的算法
7
38
810
2744
45265
最底下一层的maxsum等于本身,我们来算第二层每个数字对应的maxsum
2从45中选,2+5=7 7从52中选,7+5=12 第一个4从26中选,4+6=10 第二个4从65中选,4+6=10
于是便得到第二层的maxsum,按着这样的规律我们可以最终得到第一层第一数字的maxsum
30
23 21
20 13 10
7 12 10 10
4 5 2 6 5
6.2最大上升子序列
给定一个序列,要求给出该序列中的递增子列的长度,数据要求序列长度≤1000,且子列中的数组不要求是原序列中连续的数字,样例{1,7,3,5,9,4,8}中最长上升子列之一是{1,3,5,8},长度为4
题目分析:
我们就按照动规的思路去想问题,分解成规模小,形式类似的子问题。记f(n)为当前n个数字的序列最长上升子列长度,那么根据递归的思路,下面就是找f(n+1)与f(n)的关系,但是我们再回过头考虑一下这样递归是否符合动规解题特点
无后效性?
由于能够满足f(n)的序列可能有多个,那么不同序列的不同,如果
的话,那么最大序列长就会增加,如果
的话,最大序列长不变,f(n+1)的值如何就与前面的子问题状态有关,从而不符合''无后效性''。我们需要寻找新的分解问题的方法。使得子问题的状态不影响后续问题的值。
求以 为终点的最长上升子序列,虽然这个问题和原问题形式上并不完全相同,但是这要这N个子问题都解决了,那么原问题也就是这N个解的最大值了。这样分解的子问题的''状态''只和变量''k''有关,而''状态''对应的''值'',是最大上升子列的值。
有了这个思路后便有以下的''状态转移方程''