基础算法小结与核心实现代码分享(动态规划、分治、贪心、回溯、分支限界)

动态规划

1.数字三角形最大路径和
这是一个典型的动态规划问题,可以使用递归和记忆化搜索来解决。定义一个二维数组dp,其中 dp[i][j] 表示从顶至位置 (i, j) 的最大路径和。通过递归计算每个位置的最大路径和,同时使用记忆化搜索避免重复计算。
状态转移方程:
dp[i][j] = triangle[i][j] + max(dp[i-1][j], dp[i-1][j-1])
这表示位置 (i, j) 处的最大路径和,要么是上一行同列的最大路径和加上当前位置的值,要么是上一行前一列的最大路径和加上当前位置的值。
时间复杂度分析:
递归和记忆化搜索的时间复杂度为 O(n^2),其中 n 是三角形的行数。这是因为我们计算每个位置的最大路径和,避免了重复计算。

#include <iostream>
using namespace std;

const int NR = 1e3 + 10;
int a[NR][NR];
int s[NR];
int r;
int ans;

void cal(){
  int sum = 0; 
  for(int i = 1; i <= r; i++) {
    sum += s[i];
  }
  if(sum > ans) ans = sum;
}

void dfs(int x, int y) {
  if(x == r) {
    cal();
    return;
  }
  for(int i = 0; i <= 1; i++){
    s[x+1] = a[x+1][y+i];
    dfs(x + 1, y + i);
  }
}

int main() {
  cin >> r;
  for(int i = 1; i <= r; i++)
    for(int j = 1; j <= i; j++)
      cin >> a[i][j];

  s[1] = a[1][1];
  dfs(1, 1);

  cout << ans << endl;;
  return 0;
}

2.爬楼梯
我们可以定义一个函数 climbStairs(n) 表示爬到第 n 级楼梯的不同方式数量。状态转移方程为:climbStairs(n) = climbStairs(n-1) + climbStairs(n-2) 这表示到达第 n 级楼梯 的 方 式数 量 等于 到达 第 n-1 级 和 n-2 级楼 梯 的 方式 数 量之 和。 基 本 情况 是
climbStairs(1) = 1 和 climbStairs(2) = 2。时间复杂度是指数级别的

3.连续子数组最大和
这是经典的最大子数组和问题,可以使用动态规划来解决。我们可以定义一个一维数组
dp,其中 dp[i] 表示以第 i 个元素结尾的子数组的最大和。通过递推计算每个位置的最
大和,最后取最大值即可。状态转移方程:dp[i] = max(nums[i], dp[i-1] + nums[i])
时间复杂度为 O(n)

#include <iostream>
#include <climits>  // 为了使用INT_MIN

using namespace std;

int main() {
    int n;
    cin >> n;

    int nums[1000];
    for (int i = 0; i < n; i++) {
        cin >> nums[i];
    }

    int maxSum = INT_MIN;  // 使用INT_MIN初始化以确保处理数组中都是负数的情况

    // 遍历所有可能的子数组
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            int currentSum = 0;
            // 计算当前子数组的和
            for (int k = i; k <= j; k++) {
                currentSum += nums[k];
            }
            // 更新最大的子数组和
            if (currentSum > maxSum) {
                maxSum = currentSum;
            }
        }
    }

    cout << maxSum << endl;
    return 0;
}

4.最长上升子序列
核心思路:这是一个典型的最长上升子序列问题,可以使用动态规划来解决。我们定
义一个一维数组 dp,其中 dp[i] 表示以第 i 个元素结尾的最长上升子序列的长度。通过
状态转移方程更新 dp[i] 的值,最后返回数组中的最大值即可。状态转移方程:
dp[i] = max(dp[i], dp[j] + 1) 其中 0 <= j < i 且 nums[j] < nums[i]

状态转移方程

for(int i = 1; i < nums.size(); i++) {
    for(int j = 0; j < i; j++) {
        if(nums[i] > nums[j]) {
            dp[i] = max(dp[i], dp[j] + 1);
        }
    }
}

由于嵌套了两层循环,时间复杂度为 O(n^2)。后面还可以通过优化使用二分查找的方法将时间复杂度降为 O(n log n)。
5. 打家劫舍
核心思路:这是一个动态规划问题,我们可以定义一个一维数组 dp,其中 dp[i] 表示打劫前 i 个房子的最高金额。通过状态转移方程更新 dp[i] 的值,最后返回数组中的最大值即可。状态转移方程:dp[i] = max(dp[i-1], dp[i-2] + nums[i]) 这表示当前房子处的最高金额,要么是前一个房子处的最高金额,要么是前两个房子处的最高金额加上当前房子的金额。
时间复杂度分析:由于只需遍历数组一次,时间复杂度为 O(n)。

状态转移方程

for(int i = 2; i < nums.size(); i++) {
    dp[i] = max(dp[i-2] + nums[i], dp[i-1]);
}

6. 最长公共子序列
核心思路:这是一个典型的最长公共子序列问题,可以使用动态规划来解决。我们定义一个二维数组 dp,其中 dp[i][j] 表示字符串 A 的前 i 个字符和字符串 B 的前 j 个字符的最长公共子序列的长度。通过状态转移方程更新 dp[i][j] 的值,最后返回数组中的最大值即可。
状态转移方程:

dp[i][j] = dp[i-1][j-1] + 1 if A[i-1] == B[j-1] dp[i][j] = max(dp[i-1][j], dp[i][j-1]) otherwise

时间复杂度分析:由于嵌套了两层循环,时间复杂度为 O(m * n),其中 m 和 n 分别是字符串 A 和 B 的长度。
7. 编辑距离
核心思路:这是一个经典的编辑距离问题,可以使用动态规划来解决。我们定义一个二维数组 dp,其中 dp[i][j] 表示将字符串 A 的前 i 个字符转换为字符串 B 的前 j 个字符所需的最小操作数。通过状态转移方程更新 dp[i][j] 的值,最后返回数组的最后一个元素即可。
状态转移方程:

dp[i][j] = dp[i-1][j-1] if A[i-1] == B[j-1] dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 otherwise

#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
string s1, s2;
int dp[50010][50010];

int main()
{
  int l1, l2;
  cin >> l1 >> s1 >> l2 >> s2;
  for (int i = 0; i <= l1; ++i)
  {
    dp[i][0] = i;
  }
  for (int i = 0; i <= l2; ++i)
  {
    dp[0][i] = i;
  }
  for (int i = 1; i <= l1; ++i)
  {
    for (int j = 1; j <= l2; ++j)
    {
      if (s1[i] == s2[j])
      {
        dp[i][j] = dp[i - 1][j - 1];
      }
      else
      {
        dp[i][j] = min(dp[i - 1][j - 1] + 1, min(dp[i][j - 1] + 1, dp[i - 1][j] + 1));
      }
    }
  }
  printf("%d", dp[l1][l2]);
  return 0;
}

时间复杂度分析:由于嵌套了两层循环,时间复杂度为 O(m * n),其中 m 和 n 分别是字符串 A 和 B 的长度。
8. 0-1 背包
核心思路:这是一个经典的 0-1 背包问题,可以使用动态规划来解决。我们定义一个二维数组 dp,其中 dp[i][j] 表示在前 i 个物品中选择,背包容量为 j 时的最大总价值。
通过状态转移方程更新 dp[i][j] 的值,最后返回数组的最后一个元素即可。
状态转移方程:

dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]) if (j >= weight[i]) dp[i][j] = dp[i-1][j] otherwise

这表示如果背包容量可以放下当前物品,那么最大总价值是不放当前物品和放当前物品的两者之间的最大值;如果背包容量放不下当前物品,那么最大总价值就等于不放当前物品时的最大值。
时间复杂度分析:由于嵌套了两层循环,时间复杂度为 O(N * V),其中 N 是物品的数量,V 是背包的容量。
9. 完全背包
核心思路:
这是一个完全背包问题,也可以使用动态规划来解决。我们定义一个一维数组 dp,其
中 dp[j] 表示背包容量为 j 时的最大总价值。通过状态转移方程更新 dp[j] 的值,最后返回数组的最后一个元素即可。
状态转移方程:dp[j] = max(dp[j], dp[j-weight[i]] + value[i]) if j >= weight[i]
时间复杂度分析:
由于只需遍历物品种类和背包容量,时间复杂度为 O(N * V),其中 N 是物品的种类,
V 是背包的容量。这里和 0-1 背包问题的区别在于,对于每一种物品,可以选择放入背包的次数不限,因此是完全背包问题

// 完全背包 朴素 
#include <cstdio>
#include <iostream>
#include <algorithm>

using namespace std;

const int NR = 1010;
int v[NR], w[NR];
int dp[NR][NR];

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++)
        scanf("%d%d", &v[i], &w[i]);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++) {
            if(j < v[i]) dp[i][j] = dp[i - 1][j];
            else{
                for(int k = 0; k * v[i] <= j; k++)
                {
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - k* v[i]] + k * w[i]);
                }
            }
        }
    printf("%d", dp[n][m]);
    return 0;
}

//优化
// dp[i][j] = max(dp[i][j], 
//        dp[i][j-v] + w,  dp[i][j-2v] + 2w, dp[i][j-3v] + 3w,..) 
// dp[i][j-v] = 
//        dp[i][j-v],  dp[i][j-2v] + w, dp[i][j-3v] + 2w,..) 

int main(){
    int n,v;
    cin >> n >> v;
    for(int i = 1; i <= n; i++){
        cin >> vv[i] >> ww[i];
    }
    for(int i = 1; i <= n; i++) {//遍历物品
        for(int j = 1; j <= v; j++){// 遍历体积 
                //考虑是否可以放,以及是否放?
                if(j < vv[i]) //体积为v的背包可以放下物品i(vv[i] ) 
                    dp[i][j] = dp[i-1][j];  //不选i,体积为j的最大值 
                else{
                    dp[i][j] = max(dp[i][j],  
                                   dp[i][j-vv[i]] + ww[i]);
                }
            }
        } 
    }
    cout << dp[n][v] << endl;
    return 0;
} 

10.多重背包
核心思路:
这是一个多重背包问题,每种物品有一定的数量限制,可以使用动态规划来解决。我们定义一个二维数组 dp,其中 dp[i][j] 表示在前 i 种物品中选择,背包容量为 j 时的最大总价值。通过状态转移方程更新 dp[i][j] 的值,最后返回数组的最后一个元素即可。
状态转移方程:

for(int i = 1; i <= n; i++)
 for(int j = 0; j <= m; j++) {
 	if(j < v[i]) dp[i][j] = dp[i - 1][j];
 	else{
 		for(int k = 0; k <= s[i] && v[i] * k <= j; k++)
 			dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i] * k] +
			w[i] * k);
 	}
 }
// 多重背包 
#include <cstdio>
#include <iostream>
#include <algorithm>

using namespace std;

const int NR = 110;
int v[NR], w[NR], s[NR];
int dp[NR][NR];

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++)
        scanf("%d%d%d", &v[i], &w[i], &s[i]);
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= m; j++) {
            if(j < v[i]) dp[i][j] = dp[i - 1][j];
            else{
                for(int k = 0; k <= s[i] && v[i] * k <= j; k++)
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i] * k] + w[i] * k);
            }
        }
    printf("%d", dp[n][m]);
    return 0;
}

时间复杂度分析:
由于嵌套了两层循环,时间复杂度为 O(N * V * num[i]),其中 N 是物品的种类,V 是背包的容量。这里和完全背包问题的区别在于,每种物品有一定的数量限制,因此是多重背包问题。
11.石子合并
核心思路:
这是一个经典的哈夫曼编码问题,也可以通过贪心算法来解决。我们可以使用一个优先队列(最小堆)来维护当前序列中的数,每次取出两个最小的数合并,并将合并后的数放回队列中,重复这个过程直到队列中只剩一个数。每次合并的代价是两个数的和,因此总的代价是所有合并的代价之和。
状态转移方程:

for(int len = 2; len <= n; len++) {
 	for(int i = 1; i <= n-len+1; i++) {
 		int j = i + len - 1;
 		dp[i][j] = INT_MAX;
 		for(int k = i; k < j; k++) {
 			dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum[i][j]);
 		}
 	}
}

时间复杂度分析:
由于每次都是取出两个最小的数进行合并,然后再插入一个数,因此每次操作的时间复杂度为 O(log n),总的时间复杂度为 O(n log n),其中 n 是序列的长度。
12.股票交易
核心思路:这是一个动态规划问题,可以通过状态机的方式进行建模。
时间复杂度分析:
由于只需遍历数组一次,时间复杂度为 O(n),其中 n 是股票价格列表的长度。

#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010;
int w[N],f[N][2];
int n;

int main(){
  scanf("%d", &n);
  for(int i=1;i<=n;i++) scanf("%d",&w[i]); 

  f[0][0]=0; f[0][1]=-1e6;
  for(int i=1; i<=n; ++i){
    f[i][0]=max(f[i-1][0],f[i-1][1]+w[i]);    
    f[i][1]=max(f[i-1][1],f[i-1][0]-w[i]);
  }
  cout<<f[n][0];
}

分治

1. 逆序对的数量
核心思路:
这是一个典型的分治问题。我们可以使用归并排序的思想,在归并的过程中计算逆序对的数量。具体步骤如下:将数组分成两半,并递归地计算每个子数组的逆序对数量。在归并的过程中,合并两个有序的子数组,同时统计跨越两个子数组的逆序对数量。
最后将结果返回。
时间复杂度:归并排序的时间复杂度是 O(n log n),其中 n 是数组的长度。

#include <iostream>

using namespace std;

const int N = 100010;

int n;
int a[N], tmp[N];

long long merge_sort(int l, int r){
    if( l >= r) return 0;
    long long ans = 0;
    int mid = (l + r) >> 1;
    ans = merge_sort(l, mid) + merge_sort(mid+1, r);

    int k=0, i=l, j=mid+1;
    while(i <= mid && j <= r){
        if(a[i] <= a[j]) tmp[k++] = a[i++];
        else{
            tmp[k++] = a[j++];
            ans += mid - i + 1; //第一个里面的i后面的所有 
        }
    }

    while(i <= mid) tmp[k++] = a[i++];
    while(j <= r) tmp[k++] = a[j++];


    for(int i=l,k=0; i <= r; i++, k++) a[i] = tmp[k];
    return ans;
}

int main(){
    cin >> n;
    for(int i = 0; i < n; i++) cin >> a[i];

    cout << merge_sort(0, n-1) << endl;
    return 0;
}

2. 最大连续子数组和
核心思路:
这道题可以通过分治的策略来解决。分治的思想是将问题划分为更小的子问题,解决子问题,然后合并子问题的解。
在这里,我们将数组分为左右两部分,分别找出左半部分和右半部分的最大子数组和,然后再考虑跨越左右两部分的最大子数组和。最终的结果是这三个值中的最大值。
具体的步骤如下:
将数组分为左右两半,分别递归求解左半部分和右半部分的最大子数组和。
对于跨越左右两部分的最大子数组,分别找出包含左半部分最右边元素的最大子数组和包含右半部分最左边元素的最大子数组。两者相加得到跨越两部分的最大子数组和。
返回左半部分最大子数组和、右半部分最大子数组和、跨越两部分的最大子数组和中的最大值。
时间复杂度:
分治算法的时间复杂度通常为 O(n log n),其中 n 是数组的长度。

#include <iostream>
#include <vector>
#include <string>
#define LL long long int
using namespace std;
vector<int> nums;
int dp[10020];
int main()
{
  int len;
  scanf("%d", &len);
  for (int i = 0; i < len; ++i)
  {
    int t;
    scanf("%d", &t);
    nums.push_back(t);
  }
  // dp[i] 表示:以 nums[i] 结尾的连续子数组的最大和
  dp[0] = nums[0];
  for (int i = 1; i < len; i++)
  {
    if (dp[i - 1] > 0)
    {
      dp[i] = dp[i - 1] + nums[i];
    }
    else
    {
      dp[i] = nums[i];
    }
  }

  // 也可以在上面遍历的同时求出 res 的最大值,这里我们为了语义清晰分开写,大家可以自行选择
  int res = dp[0];
  for (int i = 1; i < len; i++)
  {
    res = max(res, dp[i]);
  }
  printf("%d", res);
  return 0;
}

3. 砍树
核心思路:
这个问题可以通过分治的策略来解决。分治的思想是将问题划分为更小的子问题,解决子问题,然后合并子问题的解。
具体的步骤如下:
首先,找到所有树的高度的中位数(Median)。
对于所有树的高度,将高度大于中位数的部分作为一个子问题,递归求解这个子问题。
在递归的过程中,通过二分查找找到一个合适的木材长度,使得大于等于这个长度的木材段数达到或接近 k。
返回递归结果。
这个方法的核心在于通过找到中位数,将问题划分为两个子问题,然后通过递归和二分查找来找到合适的木材长度。
时间复杂度:
分治算法的时间复杂度通常为 O(n log n),其中 n 是树的数量。

#include <string>
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#define LL long long int
using namespace std;
const int N = 10000020;
int a[N];

int check(int n, int l)
{
  int sum = 0;
  for_each(a, a + n, [&l, &sum](int e)
           { sum += e / l; });
  return sum;
}

int main(int argc, char **argv)
{
  ios::sync_with_stdio(false);
  int n, k;
  cin >> n >> k;
  for (int i = 0; i < n; ++i)
    cin >> a[i];
  int i = 0, j = 0;
  for_each(a, a + n, [&j](int e)
           { j = max(j, e); });
  while (i < j)
  {
    int mid = (i + j + 1) >> 1;
    if (check(n, mid) < k)
    {
      j = mid - 1;
    }
    else
    {
      i = mid;
    }
  }
  cout << i << endl;
  return 0;
}

4. 数列分段
可以使用分治的思想来解决。分治的基本思想是将原问题划分为更小的子问题,分别解决子问题,最后合并子问题的解。
在这个问题中,我们可以使用二分查找来确定每一段的和的最大值。具体步骤如下:
确定二分查找的左右边界,左边界为数组中的最大值,右边界为数组的和。
在每一次二分查找中,计算中间值 mid,并判断是否能够将数组分成 M 段,使得每段的和不超过 mid。
如果可以,则说明 mid 过大,继续在左半部分查找;如果不行,则说明 mid 过小,继续在右半部分查找。
最终返回二分查找的结果。
时间复杂度:
由于二分查找的时间复杂度是 O(log(sum(nums) - max(nums))),其中 sum(nums) 是数组的和,max(nums) 是数组中的最大值,因此总的时间复杂度是 O(log(sum(nums) - max(nums)))。

贪心

1. 采购礼物
核心思路:
这是一个典型的贪心算法问题。贪心算法的基本思想是每次都选择当前情况下的最优解,
期望通过局部最优解达到全局最优解。
在这个问题中,我们可以按照礼物的价格从低到高排序,然后依次购买,直到小红的钱不够为止。
步骤:
对所有礼物按价格进行排序。
从最便宜的礼物开始,逐个购买,直到小红的钱不够为止。
时间复杂度:
由于排序的时间复杂度为 O(n log n),其中 n 是礼物的数量,贪心算法的购买过程的时间复杂度是 O(n),因此总的时间复杂度为 O(n log n)。
2. 部分背包
核心思路:
这是一个经典的贪心算法问题,通常被称为分数背包问题。贪心算法的基本思想是每次都选择当前情况下的最优解,期望通过局部最优解达到全局最优解。
在这个问题中,我们可以按照金币的单位价值(价值/重量)从高到低进行排序,然后依次选择单位价值最高的金币放入背包。
步骤:
计算每堆金币的单位价值。
按照单位价值从高到低对金币进行排序。
依次选择单位价值最高的金币放入背包,直到背包装满或没有更多金币。
时间复杂度:
由于排序的时间复杂度为 O(n log n),其中 n 是金币的数量,贪心算法的选择过程的时间复杂度是 O(n),因此总的时间复杂度为 O(n log n)。
3. 区间选点
核心思路:
这是一个典型的贪心算法问题,通常被称为区间调度问题。贪心算法的基本思想是每次都选择当前情况下的最优解,期望通过局部最优解达到全局最优解。
在这个问题中,我们可以按照区间的结束点从小到大进行排序,然后依次选择结束点最小的区间,将点放在该区间的结束点上。这样,可以保证每个区间至少包含一个选出的点。
步骤:
对所有区间按照结束点从小到大进行排序。
从第一个区间开始,选择结束点作为一个选出的点,并移动到下一个未覆盖的区间的起始点。
重复步骤 2,直到所有区间都被覆盖。
时间复杂度:
由于排序的时间复杂度为 O(N log N),其中 N 是区间的数量,贪心算法的选择过程的时间复杂度是 O(N),因此总的时间复杂度为 O(N log N)。
4. 数列分段
核心思路:
这是一个典型的贪心算法问题,通常被称为分割数列问题。贪心算法的基本思想是每次都选择当前情况下的最优解,期望通过局部最优解达到全局最优解。
在这个问题中,我们可以按照顺序遍历数列,尽量将数列划分成若干连续的子段,使得每个子段的元素和不超过 M。具体步骤如下:
从头到尾遍历数列,累加元素,直到累加和超过 M。
当累加和超过 M 时,表示当前子段结束,记录这个子段的长度,并重新开始累加。
重复步骤 1 和步骤 2,直到遍历完整个数列。
时间复杂度:
由于只需遍历一次数列,时间复杂度为 O(N),其中 N 是数列的长度。
5. 仓库选址
核心思路:
这是一个经典的问题,通常被称为最佳位置选择问题。解决这个问题的关键是找到一个位置,使得到所有商店的距离之和最小。这个位置就是货仓的最佳位置。
一种有效的方法是通过排序坐标数组,找到中位数对应的坐标作为货仓的位置。这是因为对于任何一个点,到其他点的距离之和最小的点就是中位数。因此,通过选择中位数的位置,可以使得总距离最小。
步骤:
对坐标数组进行排序。
选择排序后的坐标数组的中位数作为货仓的位置。
时间复杂度:
由于只需对坐标数组进行一次排序,时间复杂度为 O(N log N),其中 N 是商店的数量。
6. 均分图书
这个问题其实是一个典型的贪心算法问题。为了达到最快的平衡,每一步都应尽量使当前堆与平均数接近。从第一堆开始,我们看它与平均值的差异,并将这个差值的图书移到第二堆,以此类推,直到最后一堆。每移动一次,都记录移动的次数,最后输出总的移动次数。
时间复杂度为 O(n)
7. 合并果子
这个问题是一个典型的贪心算法问题,也被称作“合并果子”或“最小堆”。基本思路是每次合并最轻的两堆果子,这样可以保证总的合并代价最小。可以采用最小堆来找到最小的果子堆。

回溯

1. 枚举子集 2
核心思路:采用一个递归函数 backtrack 生成所有可能的子集,并按照规定的排序方式输出。排序的规定是,如果两种情况前 k-1 个数相同,那么第 k 个数的选取情况在前。
为了实现这一点,首先将输入列表 nums 进行排序。
在 backtrack 函数中,每次将当前子集加入结果集,然后从当前位置开始选择下一个数加入子集。在选择下一个数之前,需要确保相同的数字相邻,避免重复的情况。因此,在选择下一个数时,需要判断当前数与前一个数是否相同,如果相同就跳过,以满足排序规定。
时间复杂度分析:
对列表 nums 进行排序的时间复杂度是 O(n log n),其中 n 是列表的长度。
递归函数 backtrack 的时间复杂度取决于生成的子集数量。在最坏情况下,每个元素都有选和不选两种可能,因此总的子集数量为 2^n,其中 n 是列表的长度。
综合考虑,整体的时间复杂度为 O(n log n + 2^n)。
2. 排列数字
解题思路:
全排列枚举的问题可以通过回溯的思想来解决。回溯是一种深度优先搜索的方法,它尝试所有可能的选择,并在不满足条件时进行回退,寻找下一个选择。对于全排列,我们可以从第一个位置开始选择所有可能的数,然后对剩余的位置进行递归地选择,直到生成一个完整的排列。
为了满足题目中的排序规定,即如果两种排列方式的前 k-1 个数相同,则将第 k 个数序号更小的排列放在前面,我们可以在递归选择的过程中进行判断和控制。每次选择一个数时,可以检查当前数是否和前一个数相同,如果相同就跳过,以确保排序规定。
时间复杂度分析:
对于全排列问题,共有 n! 种排列方式,其中 n 是数的个数。
回溯算法会尝试所有可能的排列,因此时间复杂度为 O(n!)。
在递归的过程中,需要进行判断和控制,但这些操作的时间复杂度相对于整体来说较小,
不影响主要的时间复杂度。
综合考虑,整体的时间复杂度为 O(n!)。
3. n-皇后
解题思路:
典型的 n 皇后问题可以通过回溯算法来解决。回溯算法是一种深度优先搜索的方法,尝试所有可能的解,当不满足条件时进行回退,寻找下一个解。对于 n 皇后问题,可以从第一行开始逐行放置皇后,确保每一行、每一列以及每条对角线上最多只有一个皇后。
在回溯的过程中,可以通过剪枝操作减少搜索空间,提高效率。例如,在每一行选择皇后的位置时,可以检查该位置是否与已放置的皇后冲突。如果冲突,则可以直接跳过该位置,不进行进一步的搜索。
另外,为了满足题目中的排序规定,即如果两个解的前 k-1 行的皇后位置相同,则在第 k 行中皇后位置更靠左的解应排在前面,可以在回溯的过程中记录已经放置的皇后的列号,以便在递归选择下一个位置时进行判断和控制。
时间复杂度分析:
对于 n 皇后问题,每一行都需要选择一个皇后的位置,因此搜索空间的大小为 n^n。
在回溯的过程中,通过剪枝操作可以减小搜索空间,但最坏情况下时间复杂度仍为O(n^n)。
综合考虑,整体的时间复杂度为 O(n^n)。
4. 01 背包
这是一个经典的背包问题,可以使用回溯算法来解决。回溯算法是一种深度优先搜索的方法,尝试所有可能的选择,当不满足条件时进行回退,寻找下一个解。
对于背包问题,每一步可以选择将某个物品放入背包或者不放入背包。在每个决策点,我们都需要考虑当前剩余的背包容量,以及放入物品对应的价值。通过递归地进行选择,最终找到满足条件的最优解。
在回溯的过程中,可以通过剪枝操作减少搜索空间,提高效率。例如,如果当前的背包容量已经不足以放入剩余的物品,可以直接停止递归,因为在这种情况下无法找到更优的解。
时间复杂度分析:
回溯算法的时间复杂度主要取决于搜索空间的大小。在最坏情况下,每个物品都有放入和不放入两种选择,因此搜索空间的大小为 2^N。
在回溯的过程中,通过剪枝操作可以减小搜索空间,但最坏情况下时间复杂度仍为O(2^N)。
综合考虑,整体的时间复杂度为 O(2^N)。

//01背包 

#include <cstdio>
#include <iostream>
#include <algorithm>

using namespace std;

const int NR = 1010;
int v[NR], w[NR];
int dp[NR][NR];

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++)
        scanf("%d%d", &v[i], &w[i]);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++) {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
            // if(j < v[i])
            //     dp[i][j] = dp[i - 1][j];
            // else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
    printf("%d", dp[n][m]);
    return 0;
}

分支界限

1. 走迷宫
分支界限算法是一种用于解决搜索问题的优化算法,通常用于在搜索树的节点上使用上界函数和下界函数,以剪枝不可能产生最优解的分支。对于迷宫问题,我们可以使用分支界限算法来寻找从起点到终点的最短路径。
状态表示: 在搜索过程中,我们需要记录当前位置和已经走过的路径。定义一个状态结构体,包含当前位置和路径信息。
上界函数: 我们可以通过估算当前状态到目标状态的最短距离来设置上界。例如,使用曼哈顿距离或欧几里得距离。
下界函数: 下界函数用于估计当前状态到目标状态的最短距离。可以采用启发式搜索
方法,如 A*算法,估算当前状态到目标状态的最短距离。
状态扩展: 在每个节点上,根据当前状态和已知的上下界,扩展子节点,即向上下左右四个方向移动,将新的状态加入搜索树。
剪枝策略: 在状态扩展过程中,通过比较上下界和已知最优解,进行剪枝操作,减少搜索空间。
搜索终止条件: 当找到一条从起点到终点的路径时,更新最优解,并在搜索时进行比较,及时终止搜索。
2. 01 背包
解题思路:
分支界限算法也可以用于解决背包问题,通过在搜索树上使用上界函数和下界函数进行剪枝操作,以提高搜索效率。以下是解决 0/1 背包问题的分支界限算法的思路:
状态表示: 在搜索过程中,我们需要记录当前的状态,即已经选择的物品和背包的剩余容量。
上界函数: 上界函数用于估计当前状态的最大价值,可以通过贪心策略来确定。例如,按照单位价值降序排列剩余未选择的物品,将背包容量依次填满。
下界函数: 下界函数用于估计当前状态的最小价值。可以根据已选择的物品的价值以及未选择物品的单位价值来进行估计。
状态扩展: 在每个节点上,根据当前状态和已知的上下界,扩展子节点,即选择或不选择当前物品,更新状态。
剪枝策略: 在状态扩展过程中,通过比较上下界和已知最优解,进行剪枝操作,减少搜索空间。
搜索终止条件: 当搜索到叶子节点时,更新最优解,并在搜索时进行比较,及时终止搜索。
3. 任务分配
解题思路:
状态表示: 在搜索过程中,需要记录当前的状态,即已经分配的任务和每个人的完成时间。
上界函数: 上界函数用于估计当前状态的最大完成时间,可以通过将未分配的任务按照当前已分配的人员的最小完成时间进行排序,然后依次将任务分配给对应的人员,得到一个可能的最大完成时间。
状态扩展: 在每个节点上,根据当前状态和已知的上界,扩展子节点,即选择未分配的任务分配给未分配的人员,更新状态。
剪枝策略: 在状态扩展过程中,通过比较上界和已知最优解,进行剪枝操作,减少搜索空间。
搜索终止条件: 当找到一种任务分配方式时,更新最优解,并在搜索时进行比较,及时终止搜索。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值