算法的定义
算法的三要素
算法由操作、控制结构、数据结构组成
算法的基本性质
- 目的性
- 分步性
- 有序性
- 有限性
- 操作性
算法的5个重要特性
- 有穷性
- 确定性
- 可行性
- 算法有零个或多个的输入
- 算法有一个或多个的摘出
算法时间复杂度计算
例题一、
``
例题二、
常见时间复杂度数量级
- 常数级:O(1)
- 对数级:O(log n)
- 线性级:O(n)
- 多项式级:O(n^c)
- 指数级:O(c^n)
- 阶乘级:O(n!)
迭代算法
迭代法也称“辗转法”,是种不断用变量的旧值递推出新值的解决问题的方法。
迭代法分为:
- 递推法(正推法):(兔子繁殖、最大公约数、振子跳台阶!)是迭代算法的最基本的表现形式,一般来讲,一种简单的递推方式是从小规模的问题推出大规模问题。
- 倒推法:(猴子吃桃问题、越沙漠)对某些特殊问题采用违反通常习惯的从后往前推解决。
兔子繁殖问题
- 问题描述
- 代码实现
辗转相除求最大公约数问题
- 代码实现
int gcd(int a, int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
猴子吃桃子问题
- 问题描述:猴子吃桃问题就是:一个猴子,看到许多的桃子,第一天吃了一半,又吃了一个,第二天也吃了一半,又吃了一个,一直这样下去,到了第10天,只剩下一个桃子了,求猴子吃桃子的过程。
- 问题分析:
假设第一天猴子吃了n个,这么说就是**((n/2+1)/2+1)/2+1…/2+1=1**(重复9次因为猴子第一天已经吃了桃子)这样子倒推过来就是**((1+1)*2+1)2…+1)2=n,第一天猴子吃了1个,我们赋值于x,x一直x=(x+1)*2重复9次就可以得出从第10天到第1天猴子吃桃的过程了。- 代码实现
int sum=1;//最后剩下1个桃
for(int i=0;i<=8;i++)//0也算一次,也就是9次
{
sum=(sum+1)*2;//重复操作
printf("猴子第%d天吃了%d桃\n",9-i,sum);
}
蛮力算法
百钱买百鸡问题
- 问题描述:公元5世纪末,我国古代数学家张丘建在他所撰写的《算经》中提出了这样一个问题:鸡翁一,值钱五;鸡母一,值钱三;鸡雏三,值钱一。百钱买百鸡,问鸡翁、母、雏各几何?
- 代码实现:
for(int i=0;i<20;i++) //鸡翁:i只
for(int j=0;j<33;j++) //鸡母:jzhi只
if(i*5 + j*3 + (100-i-j)/3.0 == 100) //鸡雏:100-i-j只
//输出结果
cout<<"方法"<<ans++<<":\n"<<"鸡翁:"<<i<<endl<<"鸡母:"<<j<<endl<<"鸡雏:"<<100-i-j<<endl;
解数字迷问题
求3个数的最小公倍数
- 题目描述:输入三个正整数,求它们的最小公倍数(LCM)
- 代码实现:
// 蛮力搜索最小公倍数
int search(int a, int b, int c) {
int lcm = -1; // 初始化为-1,表示没有找到解
for (int i = 1; i <= a * b * c; i++) { // 从1到三数的乘积逐个尝试
if (i % a == 0 && i % b == 0 && i % c == 0) { // 如果i是三数的公倍数
lcm = i; // 更新最小公倍数
break; // 找到一解即可退出循环
}
}
return lcm;
}
狱吏问题
水仙花数问题
- 题目描述:水仙花数是指一个n位数(n≥3),它的每个位上的数字的n次幂之和等于它本身(例如:13 + 53+ 3^3 = 153)
- 代码实现:
// 蛮力搜索3位水仙花数
int search() {
for (int i = 100; i < 1000; i++) { // 枚举所有3位数
int sum = 0;
int remainder = i;
for (int j = 1; j <= 3; j++) {
int digit = remainder % 10; // 提取低位上的数字
remainder /= 10;
sum += pow(digit,3); // 累加幂
}
// 如果满足水仙花数定义,输出
if (sum == i) printf("%d\n", i);
}
}
四皇后问题
贴纸问题
分治算法
- 解题步骤:
- 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题。
- 解决:若子问题规模较小而容易被解决则直接解,否则再继续分解为更小的子问题,直到容易解决。
- 合并:将已求解的各个子问题的解,逐步合并为原问题的解。
- 二分法所能解决的问题一般具有以下几个特征:
- 该问题的规模缩小到- -定的程度就可以容易解决。
- 该问题可以分解为若干个规模较小的相同问题。
- 该问题所分解出的各个子问题是相互独立的。
- 利用分解出的子问题的解可以合并为该问题的解。
金块问题
- 问题描述:老板有一袋金块(共n块,n是2的幂(n>=2)),最优秀的雇员得到其中最重的一块,最差的雇员得到其中最轻的一块。假设有一架比较重量的机器,希望用自己最少的比较次数找出最重和最轻的金块,并对自己的程序做复杂性分析。
- 代码实现:
int a[10]={5,3,7,9,1,6,2,8,4,0};
void MaxMin(int l , int r , int &fmax , int &fmin){
int mid = (l+r)/2; //取中间
//如果区间只有1个元素
if(l==r) fmax=fmin=a[mid];
//区间有2个元素
else if(r-l == 1) a[l]<a[r] ? fmax=a[r],fmin=a[l] : fmax=a[l],fmin=a[r];
//区间有多个元素
else
{
int lmax,lmin, //左边区间的最大最小值
rmax,rmin; //右边区间的最大最小值
//分左右区间讨论
MaxMin(l,mid,lmax,lmin);
MaxMin(mid+1,r,rmax,rmin);
//两个区间合并讨论
fmax = max(lmax , rmax);
fmin = min(lmin , rmin);
}
}
二分查找问题
- 代码实现:
/* 二分查找问题 */
int binary_search(int arr[], int left, int right, int key) {
while(left <= right) { // 在区间[left, right]里二分查找关键字key
int mid = left + (right - left) / 2; // 取中间位置
if(arr[mid] == key) {
return mid;
} else if(arr[mid] < key) { // 如果要查找的值在右半部分
left = mid + 1; // 查找右半部分
} else { // 否则在左半部分
right = mid - 1; // 查找左半部分
}
}
return -1; // 没有找到
}
残缺棋盘问题
- 问题描述
1、在棋盘当中存在一个残缺的格子,我们需要用多个三格板将次棋盘覆盖。
2、给出一个4*4的残缺棋盘摆放方法。(彩色为三格板位置,图中5种不同的颜色对应5块三格板)
- 代码实现:(一个8*8的棋盘,残缺位置为第2行第1列,注意:下面代码数组下标从0开始)
#include <bits/stdc++.h>
#define MAX 100
using namespace std;
int board[MAX][MAX];
int three_plate_num = 1;
void fun(int x,int y,int x0,int y0,int n){
//当只有一行一列时会退出
if(n<=1) return;
int three_plate_id = three_plate_num ++; //三格板编号
int mid = n/2; //减小问题的规模
//此时中点位置为 [x+mid][y+mid]
/*残缺的在左上方时 */
if(x0 < x+mid && y0 < y+mid){
fun(x , y , x0 , y0 , mid); //对左上方的棋盘递归
fun(x , y+mid , x+mid-1 , y+mid , mid); //对左下方的棋盘递归
fun(x+mid , y+mid , x+mid , y+mid-1 , mid); //对右下方的棋盘递归
fun(x+mid , y , x+mid , y+mid , mid); //对右上方的棋盘递归
board[x+mid-1][y+mid] = three_plate_id; //为左下方的棋盘赋值
board[x+mid][y+mid] = three_plate_id; //为右下方的棋盘赋值
board[x+mid][y+mid-1] = three_plate_id; //为右上方的棋盘赋值
}
/*残缺的在左下方时 */
if(x0 < x+mid && y0 >= y+mid){
fun(x , y+mid , x0 , y0 , mid); //对左下方的棋盘递归
fun(x , y , x+mid-1 , y+mid-1 , mid); //对左上方的棋盘递归
fun(x+mid , y+mid , x+mid , y+mid , mid); //对右下方的棋盘递归
fun(x+mid , y , x+mid , y+mid-1 , mid); //对右上方的棋盘递归
board[x+mid-1][y+mid-1] = three_plate_id; //为左上方的棋盘赋值
board[x+mid][y+mid] = three_plate_id; //为右下方的棋盘赋值
board[x+mid][y+mid-1] = three_plate_id; //为右上方的棋盘赋值
}
/*残缺的在右上方时 */
if(x0 >= x+mid && y0 < y+mid){
fun(x+mid , y , x0 , y0 ,mid); //对左上方的棋盘递归
fun(x , y+mid , x+mid-1 , y+mid , mid); //对左下方的棋盘递归
fun(x+mid , y+mid , x+mid , y+mid-1 , mid); //对右下方的棋盘递归
fun(x , y , x+mid-1 , y+mid-1 , mid); //对左上方的棋盘递归
board[x+mid-1][y+mid] = three_plate_id; //为左下方的棋盘赋值
board[x+mid][y+mid] = three_plate_id; //为右下方的棋盘赋值
board[x+mid-1][y+mid-1] = three_plate_id; //为左上方的棋盘赋值
}
/*残缺的在右下方时 */
if(x0 >= x+mid && y0 >= y+mid){
fun(x+mid , y+mid , x0 , y0 , mid); //对右下方的棋盘递归
fun(x , y+mid , x+mid-1 , y+mid ,mid); //对左下方的棋盘递归
fun(x , y , x+mid-1 , y+mid-1 ,mid); //对左上方的棋盘递归
fun(x+mid , y , x+mid , y+mid ,mid); //对右上方的棋盘递归
board[x+mid-1][y+mid] = three_plate_id; //为左下方的棋盘赋值
board[x+mid-1][y+mid-1] = three_plate_id; //为左上方的棋盘赋值
board[x+mid][y+mid-1] = three_plate_id; //为右上方的棋盘赋值
}
}
/*打印坐标上的点*/
void print_board(int n){
for(int i=0 ; i<n ;i++){
for(int j=0 ; j<n ; j++) cout<<board[i][j]<<"\t";
cout<<endl;
}
}
int main(){
//以上面的样例执行一下效果
fun(0,0,1,0,8);
print_board(8);
}
求最大子段和问题
- 问题描述:给定一个数组,求这个数组的连续子数组中,最大的那一段的和。
如数组[-2,1,-3,4,-1,2,1,-5,4]的最大字段和是[4,-1,2,1],为6- 代码实现:
/* 求最大子段和问题 */
int maxSubArraySum(int arr[], int n) { // 求一个数组中的最大连续子数组之和
int max_so_far = arr[0];
int curr_max = arr[0];
for(int i = 1; i < n; i++) {
curr_max = max(arr[i], curr_max + arr[i]);
max_so_far = max(max_so_far, curr_max); // 遍历数组并更新最大连续子数组之和
}
return max_so_far;
}
求一组数中的第k小的数
- 问题描述:求出第k小的数
- 代码实现
/* 求一组数中的第k小的数 */
int select_k(int arr[], int left, int right, int k) { // 选出一个数组中第k小的元素
if(left == right) { // 如果数组中只有一个元素
return arr[left]; //直接返回这个元素
}
int pivotIndex = left + (right - left) / 2; // 选取枢纽元
pivotIndex = partition(arr, left, right, pivotIndex); // 将数组分为两部分
if(k == pivotIndex) {
return arr[k]; //找到第k小的元素
} else if(k < pivotIndex) {
return select_k(arr, left, pivotIndex - 1, k); //递归在左面的部分寻找第k小的元素
} else {
return select_k(arr, pivotIndex + 1, right, k); //递归在右面的部分寻找第k小的元素
}
}
贪心算法
- 概念:
- 贪心法(Greedy)又叫登山法,它的根本思想是逐步到达山顶,即逐步获得最优
- 贪心算法设计思想:
- 贪心算法的基本思想是找出整体当中每个小的局部的最优解,并且将所有的这些局部最优解合起来形成整体上的一个最优解。
- 能够使用贪心算法的问题必须满足下面的两个性质:
- 整体的最优解可以通过局部的最优解来求出。
- 一个整体能够被分为多个局部,并且这些局部都能够求出最优解。
币种统计问题
- 问题描述:
- 代码实现:
/* 币种统计问题 */
int changeCoins(int coins[], int n, int k) { // 计算金额k可能的组合数
int dp[k + 1];
memset(dp, 0, sizeof(dp)); // 初始化数组
dp[0] = 1; // 只有一种表示 1 元的方法,即 1 个 1 元硬币
for(int i = 0; i < n; i++) { // 从前往后遍历硬币的面额
for(int j = coins[i]; j <= k; j++) { // 在所有的金额中找到该面额可表示的组合数
dp[j] += dp[j - coins[i]];
}
}
return dp[k]; //返回表示金额k的组合数
}
动态规划的基本思想
- 基本思想:
- 将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到的子问题往往不是互相独立的。
- 求解步骤:
- 分析最优子结构性质。
- 递归地定义最优值。
- 以自底向上的方式计算出最优值。
- 根据计算最优值时得到的信息,构造最优解。
上台阶问题
- 问题描述:
- 解题思路:这是一个求斐波那契数列题。
- 代码实现:
int fibonacci(int n){
int dp[n+1];
//边界
dp[0]=dp[1]=1;
for(int i=2;i<=n;i++)
dp[i] = dp[i-1]+dp[i-2];
return dp[n];
}
路径数问题
- 问题描述:一个机器人位于一个
m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
代码实现:
int DP(int m , int n){
int dp[m][n]; //m行n列
//边界,第一行和第一列只有一种方法可以到达
for(int i=0;i<m;i++) dp[i][0]=1;
for(int i=0;i<n;i++) dp[0][i]=1;
for(int i=1;i<m;i++)
for(int j=1;j<n;j++)
f[i][j] = f[i-1][j] + f[i][j-1];
return dp[m-1][n-1];
}
路径数值最小问题
- 问题描述:给定一个有权重的有向图,求从起点到终点的所有路径中,路径权重之和最小的长度。
- 由图可判断最小路径和为:1+3+1+1+1 =7
- 代码实现:
int a[N][N]; //用来存放二维数组当中的权重
int DP(int n){
int dp[n][n];
//边界
dp[0][0] = a[0][0]; //起点无选择
//到第一列的元素只能由上一元素向下移动得到
for(int i=1;i<n;i++) dp[i][0] = dp[i-1][0] + a[i][0];
//到第一行的元素只能由上一元素向右移动得到
for(int i=1;i<n;i++) dp[0][i] = dp[0][i-1] + a[0][i];
for(int i=1;i<n;i++)
for(int j=1;j<n;j++)
dp[i][j] = min(dp[i-1][j] , dp[i][j-1]) + a[i][j];
return dp[n-1][n-1];
}
最大子段和问题
- 问题描述:给定一个数组,求这个数组的连续子数组中,最大的那一段的和。
如数组[-2,1,-3,4,-1,2,1,-5,4]的最大字段和是[4,-1,2,1],为6。- 解题思路:状态转移方程:
result=max(a[i] , result+a[i])
- 代码实现:
int a[N+1]; //存放数组
int DP(int n){
int result;
int sum=0;
for(int i=1;i<=n;i++){
// 不取前面的,取前面的
result = max(a[i] , result+a[i]);
sum = max(sum , result); //更新最大和
}
return sum;
}
0-1背包问题
- 问题描述:有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
- 代码实现:(特别说明:如果使用二维数组,j的枚举顺序无关紧要。若使用的是一维数组,那么j必须要逆序枚举)
//二维数组
int DP(int n,int m){ //物品数量,背包最大容量
int dp[n+1][m+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) //j用来表示背包当前的容量
// 如果当前背包容量不足以装下当前物品,那么最大价值等于上一个状态的最大价值
if(j<w[i]) dp[i][j]=dp[i-1][j];
// 可以装下当前物品,比较不装入该物品的价值和装入该物品后的价值,取最大值
else dp[i][j] = max(dp[i-1][j] , dp[i-1][j-w[i]]+v[i]);
//由于i、j都是递增遍历,只需输出最后一个即可
return dp[n][m];
}
//一维数组
int DP(int n,int m){
int dp[m+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
for(int j=m;j>=w[i];j--)
dp[j] = max(dp[j] , dp[j-w[i]]+v[i]);
int result = dp[0];
for(int i=1;i<=m;i++)
result = max(result , dp[i]);
return result;
}
最长公共子序列(LCS)
- 问题描述:
输入: str1 = "abcde", str2 = "ace"
最长公共子序列是 "ace":它的长度是 3
- 图片演示:
- 代码实现:
int LCS(char s1[] ,char s2[] , int len1 , int len2){
int dp[len1+1][len2+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<=len1;i++)
for(int j=1;j<=len2;j++)
if(s1[i] == s2[j]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i-1][j] , dp[i][j-1]);
return dp[len1][len2];
}
数塔问题:
- 问题描述:有一个n行的数塔,数塔上有若干数字。问从数塔的最高点到底部,在所有的路径中,经过的数字的和最大为多少?如图:
- 其中7—3—8—7—5的路径经过数字和最大,为30。解题思路:
- 面对数塔问题,使用贪心算法显然是行不通的,比如给的样例,如果使用贪心算法,那选择的路径应当是7—8—1—7—5,其经过数字和只有28,并不是最大。
- 而用深搜DFS很容易算出时间复杂度为 O(2n) (因为每个数字都有向左下和右下两种选择),行数一多必定超时。
- 动态规划可以选择从上到下,也可以从下到上。值得注意的是,从上到下要注意数组下标问题。
- 代码实现:
//动态方法:
int a[N]; //存放数塔
int DP(int n){
for(int i=n-1;i>0;i--) //从下往上
for(int j=1;j<=i;j++)
if(a[i+1][j] >= a[i+1][j+1]) //判断左下和右下的谁更大
a[i][j] += a[i+1][j];
else
a[i][j] += a[i+1][j+1];
return a[1][1]; //此时的顶层是底层累加上去的,故为最大
}
回溯算法设计思想
- 概念:
- 有“通用的解题法”之称,可以系统地搜索一个问题的所有解和任意解。
- 有递归和非递归之分
批处理作业调度问题
- 问题描述:给定 n 个作业的集合 j =( j1 ,j2,···, jn )。每一个作业 j [i]都有两项任务分别在两台机器上完成。每一个作业必须先由机器1处理,然后由机器2处理。作业 j [i]需要机器 j 的处理时间为 t [j] [i],其中 i =(1,2,···, n ), j =(1,2)。对于一个确定的作业调度,设 F [j] [i]是作业 i 在机器 j 上的完成处理的时间。所 有作业在机器2上完成处理的时间之和 f = sigma F [2] [i]称为该作业调度的完成成时间之。
- 代码实现:
#include <bits/stdc++.h>
using namespace std;
int x[100]; //作业调度顺序
int bestx[100]; //当前最优作业调度
int m[100][3];
//各作业所需的处理时间
//m[j][i]代表第j个作业在第i台机器上的处理时间
int f1=0;//机器1完成处理时间
int f2=0;//机器2完成处理时间
int cf=0;//完成时间和
int bestf=10000;//当前最优值,即最优的处理时间和
int n;//作业数量
void Backtrack(int t)
{ //t用来指示到达的层数(第几步,从0开始),同时也指示当前执行完第几个任务/作业
int tempf,j;
if(t>n) //到达叶子结点,搜索到最底部
{
if(cf<bestf) //是更优解
{
//更新最优调度序列
for(int i=1; i<=n; i++)
bestx[i]=x[i];
//更新最优目标值
bestf=cf;
}
}
else //非叶子结点
{
for(j=t; j<=n; j++) //j用来指示选择了哪个任务/作业(也就是执行顺序)
{
f1+=m[x[j]][1];//选择第x[j]个任务在机器1上执行,作为当前的任务
tempf=f2;//保存上一个作业在机器2的完成时间
f2=(f1>f2?f1:f2)+m[x[j]][2];//保存当前作业在机器2的完成时间
cf+=f2; //在机器2上的完成时间和
//如果该作业处理完之后,总时间已经超过最优时间,就直接回溯。
//剪枝函数
if(cf<bestf) //总时间小于最优时间
{
swap(x[t],x[j]); //交换两个作业的位置,把选择出的原来在x[j]位置上的任务调到当前执行的位置x[t]
Backtrack(t+1); //深度搜索解空间树,进入下一层
swap(x[t],x[j]); //进行回溯,还原,执行该层的下一个任务 //如果是叶子节点返回上一层
}
//回溯需要还原各个值
f1-=m[x[j]][1];
cf-=f2;
f2=tempf;
}
}
}
int main(){
int i,j;
cout<<"请输入作业数:"<<endl;
cin>>n;
cout<<"请输入在各机器上的处理时间"<<endl;
for(i=1; i<=2; i++) //i从1开始
for(j=1; j<=n; j++)
cin>>m[j][i];//第j个作业,第i台机器的时间值
for(i=1; i<=n; i++)
x[i]=i;//初始化当前作业调度的一种排列顺序
Backtrack(1);
cout<<"调度作业顺序:"<<endl;
for(i=1; i<=n; i++)
cout<<bestx[i]<<' ';
cout<<endl;
cout<<"处理时间:"<<endl;
cout<<bestf;
return 0;
}
n皇后问题
- 问题描述:一个如下的 6 × 6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得>每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。
- 代码实现:
#include <bits/stdc++.h>
using namespace std;
#define N 15 //最大皇后数量
int q[N]; //存放皇后的列下标
int n; //皇后的个数
int ans = 0; //答案个数
//判断i行j列是否可以放皇后
int isOK(int i,int j){
for(int k=1;k<i;k++) //遍历q数组
if(j==q[k] || abs(i-k)==abs(j-q[k])) return 0;
//列相等 或者 横纵坐标的距离相等(即在对角线上)
return 1;
}
//放置皇后到棋盘上(从第i行开始)
void place(int i)
{
//放置了n个皇后
if(i>n) { //打印结果,输出q数组即可
if(ans++ <3){ //按题目要求,打印3次
for(int i=1;i<=n;i++)
cout<<q[i]<<" ";
cout<<endl;
}
} else {
//试探列的位置
for(int j=1;j<=n;j++){
if( isOK(i,j) ){
q[i]=j;
place(i+1);//继续放置下一行的皇后
}
}
}
}
int main (){
cin>>n;
place(1); //注意:题目的下标从1开始
cout<<ans;
}