算法问题
回溯法
N-皇后问题
给定一个N*N棋盘,要在棋盘上摆放N个皇后,并且满足N个皇后中的任意两个皇后都不处于同一行,同一列,同一条斜线同一条反斜线
q[i]表示第i个皇后在第i行上的第q[i]列, 例如:
q[4] = 3 # 第四行的皇后在第三列
- 判断同一列:
q[j] == q[i] - 判断同一斜线: 行号列号插值相同
abs(i-j)
非递归方法求解N皇后
#include <stdio.h>
#include <math.h>
#define N 4
int q[N+1];
int check(int j){
int i;
for (i=1; i<j; i++){
if(q[i] == q[j] || abs(q[i] - q[j]) == abs(i - j))
{
return 0;
}
return 1;
}
}
void queen(){
int i;
for (i =1; i < N; i++)
{
q[i] = 0; // 初始化列表使摆放位置为0
}
int answer = 0;
int j = 1;
while(j >= 1){
q[j] = q[j] + 1;
while(q[j] <= N && !check(j)) // 如果j=N证明没有位置可以摆放n 如果check值为说明该位置不能摆放j
{
q[j] = q[j] + 1; // 不合法向后挪1位
}
if(q[j] <= N){ // 找到摆放皇后Q的位置了
if (j == N){ // 找到了全部位置
answer += 1;
printf("方案%d:", answer);
for (i = 1; i <= N; i++){
printf("%d", q[i]);
}
printf("\n");
}else{
// 方案没有结束,继续下一行
j += 1;
}
}else{ // 这个方案没有皇后q的位置回溯到上一行
j -= 1;
}
}
}
int main(){
queen();
return 0;
};
递归方法求N皇后解
#include <stdio.h>
#include <math.h>
#define N 50
int answer = 0;
int q[N+1];
int check(int j){
int i;
for (i = 1; i < j; i++) {
if(q[i] == q[j] || abs(i-j) == abs(q[i] - q[j]))
return 0;
}
return 1;
}
void queen(int j) {
int i;
for (i = 1; i <= N ; i++) {
q[j] = i;
if(check(j)){
if(j == N){
answer += 1;
printf("方案%d:", answer);
for (i =1; i < N; i++){
printf("%d", q[i]);
}
printf("\n");
}else{
queen(j+1); // 递归摆放下一个皇后的位置
}
}
}
}
int main() {
queen(1);
return 0;
}
分治法
递归
递归是指子程序(或函数)直接调用自己或者通过一些列语句间接调用自己,是一种描述问题和解决问题的常用方式
递归有两个基本要素:
- 边界条件,即确定递归何时终止,也成为递归出口
- 递归模式:即大问题如何分解为小问题的,也称为递归体
分治法的基本思想
[分治法]得基本思想是将一个规模为n的问题分解为k个规模为m的相互独立且与原问题解法相同的子问题,然后将子问题的解合并得到原问题的解。如果规模为n的问题可以分解为k个子问题,1<k<=n,这些子问题相互独立且与原问题相同。分治法产生的子问题往往是原问题的较小模式,这就为递归技术提供了方便。
一般来说,分治算法在每一层的递归都有3个步骤。
- 分解,将原问题分解为一系列子问题
- 求解,递归求解各个子问题。若子问题足够小,则直接求解
- 合并,将子问题的解合并为原问题的解。
分治法的典型实例
归并排序
//
// Created by qq154 on 2023/10/15.
//
#include <stdio.h>
#include <limits.h>
void Merge(int A[], int start, int mid, int end){
// 合并数组
int n1 = mid - start + 1, n2= end - mid, i, j, k;
int L[50], R[50];
for (i = 0; i < n1; i++) {
L[i] = A[start + i];
}
for (j = 0; j < n2;j++) {
R[j] = A[mid + 1 + j];
}
L[n1] = 100;
R[n2] = 100;
i = 0;
j = 0;
for(k=start; k <= end; k++){
if(L[i] < R[j]){
A[k] = L[i];
i++;
}else{
A[k] = R[j];
j++;
}
}
}
void MergeSort(int A[], int p , int r){
int q;
if(p < r){
q = (p + r) / 2;
MergeSort(A, p, q);
MergeSort(A, q+1, r);
Merge(A,p,q,r);
}
}
int main(){
int A[] = {2,4,5,7,0,1,6,9,8,3,1};
MergeSort(A, 0, 10);
for (int j = 0; j < 10 ; j++) {
printf("%d", A[j]);
}
}
最大子段和问题
//
// Created by qq154 on 2023/10/16.
//
#include <stdio.h>
int MaxSubSum(int * Array, int left, int right){
int sum = 0;
int i;
if(left == right){
if(Array[left] > 0)
sum = Array[left];
else
sum = 0;
} else{
/*从left和right的中间分解数组*/
int center = (left+right)/2;
int leftsum = MaxSubSum(Array, left, center);
int ringhtsum = MaxSubSum(Array, center+1, right);
int s1 = 0;
int lefts = 0;
for (i = center;i>=left;i--){
lefts = lefts + Array[i];
if(lefts>s1)
s1=lefts;
}
int s2 = 0;
int rights = 0;
for (int j = center+1; j < right; j++) {
rights = rights+Array[j];
if (rights > s2)
s2 = rights;
}
/*情形1*/
sum = s1+s2;
if(sum < leftsum)
sum = leftsum;
if(sum<ringhtsum)
sum = ringhtsum;
}
return sum;
}
int main(){
int sum;
int Array[] = {-1, -5, 10, 16, 5, 8, 7, 16};
sum = MaxSubSum(Array, 0, 7);
printf("%d", sum);
return 0;
}
动态规划法
动态规划法思想
动态规划法与分治法类似,其基本思想也是将待求解问题分解为若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合用动态规划法求解的问题,经分解得到的子问题不是独立的。如果用分治法求解这些问题,则相同的子问题会被求解很多次,以至于最后解决问题需要耗费指数级时间。然而,不同子问题的数目常常只有多项式量级。如果能够保存已经解决的子问题的答案,在需要时再找出以求得的答案,这样就可以避免大量重复计算,从而得到多项式时间的算法。为了达到这个目的,可以用一个表来记录所有已解决的子问题的答案。不管子问题以后是否被用到,只要他被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
动态规划法基本步骤:
- 找出最优解的性质,并刻画其结构特征。
- 递归地定义最优解的值
- 以自底向上的方式计算出最优值。
- 根据计算最优值时得到的信息,构造一个最有解。
动态规划法是一个非常有效的算法设计技术,动态规划法可以解决的问题具有两个以下特征:
- 最有子结构。如果一个问题的最优解中包含了其子问题的最优解,就说明该问题具有最优子结构。当一个问题具有最优子结构时,提示我们动态规划可能会适用,但此时贪心策略可能也是适用的。
- 重叠子问题。重叠子问题指用来求解原问题的递归算法可以反复的解同样的子问题,而不是总在产生新的子问题。即当一个递归算法不断地
动态规划的典型实例
0-1背包问题
有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
| 物品编号 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 物品价值 | 2 | 4 | 5 | 6 |
| 物品重量 | 1 | 2 | 3 | 4 |
| 物品数量=4 | 背包容量 = 5 |
|---|
问题分解
| 数量\容量 | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 2 | 2 | 2 | 2 | 2 |
| 2 | 0 | 2 | 4 | 6 | 6 | 6 |
| 3 | 0 | 2 | 4 | 6 | 7 | 9 |
| 4 | 0 | 2 | 4 | 6 | 7 | 9 |
不选第i个物品:
等于从前i-1个物品中选,背包容量为j时的最大价值
f[i][j]=f[i-1][j]
选第i个物品
前提条件:背包容量j大于等于第i个物品的重量才能选
if(j >= w[i])
等于 第i个物品的价值 加上 从前i-1个物品中选
背包容量 (j减去第i个物品的重量)时的最大价值
f[i][j] = v[i] + f[i-1][j-w[i]]
当选第i个物品时,要考虑第i个物品 和 不选第 i个物品两种情况的较大值作为 f[i]f[j]的最优解
等于 f[i][j] = max(f[i-1][j], f[i-1][j-w[i]]+v[i])
0-1背包问题实现代码:
//
// Created by qq154 on 2023/10/17.
//
#include <stdio.h>
#define W 5 // 背包容量
#define N 4 //物品数量
int max(int a, int b){
return a > b ? a: b;
}
int main(){
int v[] = {0, 2, 4, 5, 6};//物品价值数组
int w[] = {0, 1, 2, 3, 4};//物品价值数组
int f[N + 1][W + 1] ={};
int i, j;
for (i = 1; i <= N; i++) {
for (j = 0; j <= W; j++) {
f[i][j] = f[i-1][j]; // 不选第i个物品
if(j >= w[i]){ //满足条件选第i个物品
f[i][j] = max(f[i][j], v[i] + f[i-1][j-w[i]]);
}
// 不选第i个物品
if(j >= w[i]){ // 选第i个物品的前提条件
f[i][j] =max(v[i] + f[i-1][j-w[i]], f[i -1][j]) ;
}else{ // 不选第i个物品
f[i][j] = f[i-1][j];
}
}
}
printf("%d\n", f[N][W]);
for (i = 0; i <= N; i++) {
for (j = 0; j <= W ; j++){
printf("%d ", f[i][j]);
}
printf("\n");
}
return 0;
}
0-1背包问题时间复杂度
双层for循环 外面需要执行n次 里面需要执行w次需要执行n*w次即
矩阵连乘问题时间复杂度为 O(n^3) 空间复杂度为O(n^2)
贪心算法
贪心算法的基本思想
贪心算法也经常用于解决最优化问题,贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。贪心算法一旦做出了选择,不管将来有什么结果,这个选择都不会改变。贪心算法是局部最优解,但通常能得到较好的近似最优解。
贪心算法在很多情况下都能得到最优解,比如著名的旅行商问题(Traveling Salesman Problem, TSP)。但需要注意的是,贪心算法不一定总是能得到全局最优解,特别是在复杂的优化问题中,比如找零问题。
以下是一个简单的贪心算法例子:
问题描述:假设有面额为1、5、10、2.5的硬币,现在需要计算出最少需要多少个硬币能凑出给定的金额。
贪心策略:总是尽可能选择最大面额的硬币。
例如,为了凑出金额13,可以按照以下方式选择硬币:
- 使用一个面额为10的硬币
- 使用一个面额为25的硬币
- 使用一个面额为1的硬币
总共使用了3个硬币。但是,实际上还可以使用两个面额为5的硬币来凑出同样的金额,只需要2个硬币。因此,上述贪心策略并不是最优解。
需要注意的是,贪心算法通常只适用于特定的问题,而且它只能保证在每一步选择中得到局部最优解,不能保证得到全局最优解。因此,在使用贪心算法时,需要仔细考虑其适用性和局限性。
贪心法具有两个性质:
- 最优子结构。
- 贪心选择性质