2020算法课程记录


前言

第一学期算法课,刷题总结
http://47.99.179.148/


1 分治法

1.1 基本思想

将一个难以解决的大问题分割为若干个规模较小的相同子问题。
由于子问题与原问题的处理方法相同,所以可以采用递归的方法来解决。

1.2 使用场景

  1. 子问题易解决
  2. 最优子结构性质:可以分为若干个规模较小的相同子问题。
  3. 分解的子问题可以再合并为该问题的解。如果满足1和2,不满足3可以考虑贪心和动态规划。
  4. 子问题相互独立:如果子问题中有交集分治法的效率就会下降,因为要考虑到合并时如何处理公共部分。所以这时候使用动态规划较好。

1.3 基本步骤

  1. 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
  2. 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
  3. 合并:将各个子问题的解合并为原问题的解
def divide_and_conquer(p):
	#1. 如果规模足够小,则解决该问题
	if len(p) <= n0:
		slove(p)
	#2. 将问题分为k个子问题
	divide(p)
	#3. 遍历这k个子问题,继续分割
	for i in range(k):
		yi = divide(pi)
	#4. 合并分割的k个子问题
	return merge(y1, y2, ..., yk)
	

1.4 时间复杂度

T ( n ) = k T ( n / m ) + f ( n ) T(n)=k T(n / m)+f(n) T(n)=kT(n/m)+f(n)

1.5 经典问题

  1. 二分搜索
  2. 大整数乘法
  3. Strassen矩阵乘法
  4. 棋盘覆盖
  5. 合并排序
  6. 快速排序
  7. 线性时间选择
  8. 最接近点对问题
  9. 循环赛日程表
  10. 汉诺塔

1.6 课程试题

1.6.1 1004 归并排序

题目:
在这里插入图片描述
思路:

  1. 先确定分割点,一般是二分或者随机分割。这道题是二分。
  2. 对左半部分和右半部分分别进行递归,即继续分割。分割其实就是向下传递处理好的数组下标。
  3. 递归到不能再分割之后,进行子问题的处理以及合并。处理即将分割的两个部分进行一起排序。
    分别创建两个新数组存放左右两部分。
    然后依次遍历这两个数组,相比较每次选出较小的元素放入原数组的相应位置。
    最后将没有遍历完的数组放在原数组的余下位置。
    注意这两个数组也是上一层递归而来的,所以也是有序的。

代码如下:

#include <iostream>
#include <vector>
#include <math.h>
using namespace std;
 
/*
1
9 9 8 7 6 5 4 3 2 1
*/
vector<int> res;

void merge(vector<int>& arr, int l, int m, int r, int count){
   if(count == 1){
      for(int i = l; i <= r; i ++){
         res.push_back(arr[i]);
      }
   }
   int n1 = m - l + 1, n2 = r - m;
   vector<int> left, right;
   for(int i = 0; i < n1; i++){
      left.push_back(arr[l + i]);
   }
   for(int i = 0; i < n2; i++){
      right.push_back(arr[m + 1 + i]);
   }
   int i = 0, j = 0, k = l;
   while(i < n1 && j < n2){
      if(left[i] < right[j]){
         arr[k++] = left[i++];
      }
      else{
         arr[k++] = right[j++];
      }
   }

   while(i < n1){
      arr[k++] = left[i++];
   }

   while(j < n2){
      arr[k++] = right[j++];
   }
}

void mergeSort(vector<int>& arr, int l, int r, int count){
   if(l < r){
      //当数组长度为偶数时,分开的两个数组长度相同
      //当数组长度为奇数时,分开的前面的那个数组短一些
      int m = (l + r) / 2;
      mergeSort(arr, l, m, count + 1);
      mergeSort(arr, m + 1, r, count + 1);
      merge(arr, l, m, r, count);
   }
}

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

      int m;
      cin >> m;

      int count = 0;
      vector<int> nums;
      for(int j = 0; j < m; j++){
         int temp;
         cin >> temp;
         nums.push_back(temp);
      }

      mergeSort(nums, 0 ,m - 1, count);
      for(int j = 0; j < res.size(); j++){
         if(j == res.size() - 1) cout << res[j];
         else cout << res[j] << ' ' ;
      }
      cout << endl;
      res.clear();
   }
}

1.7 总结

在写代码时,先写好整个框架。可以拿归并排序的代码作为一个模板。
先写主函数main,获取数据和调用方法
再写分割函数mergeSort

  1. 分割点,m = l + r >> 1
  2. 分割函数,一般就是递归这个函数
  3. 合并函数,用于处理合并子问题
    最后实现处理子问题的函数merge

2 动态规划

2.1 基本思想

和分治法类似,都是将复杂问题分割为若干个子问题。
不过动态规划的子问题为纵向的子问题,可以使用前一子问题的信息来解决后一子问题,从而避免重复计算,降低了时间复杂度。

2.2 使用场景

  1. 子问题存在交集且为纵向
  2. 当前状态(子问题)可能与之前的好几个状态(子问题)存在关联
  3. 存在最优子结构性质
    知乎某网友:状态转移树中,若后一状态仅仅取决于上一个状态,就用贪婪算法;若后一状态取决于之前的多个状态,就用动态规划。

2.3 基本步骤

  1. 自顶向下(备忘录)
    自顶向下递归,建立一个数组(备忘录/哈希表)来记录之前计算过的值
    每一次计算前检查备忘录,如果算过了就可以拿来直接用
  2. 自底向上
    自底向上计算,也就是一个填表的过程。表中的每一个位置都看做一个独立的状态(子问题)来解决
    解决每个子问题可以用到之前解决过的子问题的结果

2.4 时间复杂度

2.5 经典问题

自顶向下(备忘录)
斐波那契数列的计算
自底向上

  1. 线性模型(这个我不太熟)
  2. 0/1背包模型
  3. 区间模型
  4. 树形模型

2.6 课程试题

2.6.1 1009 拦截导弹1

题目:
在这里插入图片描述

思路:
0/1背包模型
状态为能够最多拦截导弹的数量
在处理第i个导弹时,扫描之前的i - 1个导弹
如果之前发射的导弹j高度小于第i个导弹则可以

  1. 选择发射该导弹,当前发射的导弹数为dp[j] + 1
  2. 不发射,当前导弹数为dp[i - 1]
    代码如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int lanjie(vector<int> nums, vector<int>& dp){
    int n = nums.size();
    for(int i = 1; i < n; i++){
        for(int j = 0; j < i; j++){
            if(nums[i] < nums[j]) dp[i] = max(dp[i - 1], dp[j] + 1);
        }
        
    }
    auto res = max_element(dp.begin(), dp.end());
    return *res;
}

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

      int m;
      cin >> m;

      int count = 0;
      vector<int> nums;
      vector<int> dp(m, 1);

      for(int j = 0; j < m; j++){
         int temp;
         cin >> temp;
         nums.push_back(temp);
      }
      cout << lanjie(nums, dp) << endl;
   }
}

2.6.2 1018 0/1背包问题2

题目:
在这里插入图片描述

思路:
0/1背包模型
状态转移方程:
f [ i ] [ v ] = max ⁡ { f [ i − 1 ] [ v ] , f [ i − 1 ] [ v − c i ] + W i } f[i][v]=\max \{f[i-1][v], f[i-1][v-c i]+W i\} f[i][v]=max{f[i1][v],f[i1][vci]+Wi}
如果要使得背包恰好装满,则其对应的子问题(子状态/容量更小的背包)也必须要恰好装满。
所以初始化第一行第一个元素为0,其他都为无穷小。
这样从第一次开始放入物品,若不是恰好装满,则物品价值加上无穷小还是为无穷小。所以最后判断如果dp[n][n]不为无穷小则说明可以全部装满。
比如,如果背包容量为4,这时候装第一个物品容量为3,则上一个状态为 f [ 0 ] [ 4 − 3 ] f[0][4 - 3] f[0][43]为负无穷小, f [ 0 ] [ 4 − 3 ] + v [ 1 ] f[0][4 - 3] + v[1] f[0][43]+v[1]还是负无穷小
如果装入第一个物品容量为4,则上一个状态为 f [ 0 ] [ 4 − 3 ] f[0][4 - 3] f[0][43]为0, f [ 0 ] [ 4 − 3 ] + v [ 1 ] f[0][4 - 3] + v[1] f[0][43]+v[1] v [ 1 ] v[1] v[1]
代码如下:

#include <iostream>
#include <vector>
#include <limits.h>
using namespace std;

int full_bag(int n, int c, vector<int> si, vector<int> vi){
    vector<vector<int>> dp(n + 1, vector<int>(c + 1, 0));
    dp[0][0] = 0;
    for(int i = 1; i < c + 1; i++){
        dp[0][i] = INT_MIN;
    }
    for(int i = 1; i < n + 1; i++)
        for(int j = 0; j < c + 1; j++){
            dp[i][j] = dp[i - 1][j];
            if(j >= si[i - 1]) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - si[i - 1]] + vi[i - 1]);
    }
    if(dp[n][c] > 0) return dp[n][c];
    else return 0;
}
//5 5 2 8 5
//6 7 8 1 9
int main()
{
    int m;
    cin >> m;
    for(int i = 0; i < m; i++){
        int n, c;
        cin >> n >> c;
        vector<int> si, vi;
        for(int i = 0; i < n * 2; i++){
            int temp;
            if(i % 2 == 0){
                cin >> temp;
                si.push_back(temp);
            }else{
                cin >> temp;
                vi.push_back(temp);
            }
        }

        cout << full_bag(n, c, si, vi) << endl;
    }
}

2.6.3 1020 矩阵连乘

题目:
在这里插入图片描述

思路:
区间模型
状态转移方程:
dp ⁡ [ i ] [ j ] = min ⁡ ( d p [ i ] [ j ] , d p [ i ] [ k ] + d p [ k + 1 ] [ j ] +  weight  [ i − 1 ] ∗  weight  [ k ] ∗  weight  [ j ] ) \operatorname{dp}[i][j]=\min (d p[i][j], d p[i][k]+d p[k+1][j]+\text { weight }[i-1] * \text { weight }[k] * \text { weight }[j]) dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+ weight [i1] weight [k] weight [j])

  1. 第一个for枚举区间长度。最少2个矩阵,最多n个。
  2. 第二个for枚举区间左端点。左端点从0开始枚举,最大可为n - 1 - len。
  3. 第三个for枚举内部的分割点。极端是分割点位0和len - 1。因为0和len的情况实际是一样的。

权重矩阵weight下标0的位置存储的是第一个矩阵的行数,1~n-1存储的是所有矩阵的列数。
当使用权重矩阵时,第i个矩阵的行数为weight[i - 1],列数为weight[i]

状态转移方程的原理就是算出以i j为两个端点,哪种分法代价最小。

O(n^2)的解法(备忘录):0v0
代码如下:

#include <iostream>
#include <vector>
#include <limits.h>
using namespace std;

int mult_matrix(int n, vector<int> weight){
    vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));

    for(int len = 2; len < n + 1; len++){
        for(int i = 1; i < n - len + 2; 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] + weight[i - 1] * weight[k] * weight[j]);
            }
        }
    }

    return dp[1][n];
}

int main()
{
    int m;
    cin >> m;
    for(int i = 0; i < m; i++){
        int n;
        scanf("%d",&n);
        vector<int> weight;
        for(int i = 0; i < n * 2; i++){
            int temp;
            scanf("%d",&temp);
            if(i % 2 == 0){
                weight.push_back(temp);
            }
            if(i == n * 2 - 1) weight.push_back(temp);
        }
    printf("%d\n", mult_matrix(n, weight));
    }

    return 0;
}

2.6.4 1024 最优二叉搜索树

题目:
在这里插入图片描述

思路:
区间模型
题意:

  1. 关键字和关键字外区间(共N个节点和N+1个区间)也有搜索概率;
  2. 关键字节点和关键字之外区间的搜索代价:该节点(区间)到树的根的路径上关键字节点的个数;
  3. 期望代价为所有关键字和关键字之外区间的期望代价之和

网上的题解在处理区间节点时路径算的是该区间节点到根节点;
而这道题处理区间节点时路径算的是其上一个关键字节点到根节点;
等于网上的题解针对这道题是多算了一遍区间节点,最后减掉即可

代码思路:

需要构建两个二维数组来存储区间的一些信息
f[i][j]表示区间(i, j)内的最小代价,e[i]表示区间(i, j)的概率总和
状态方程为:
f[i][j] = max{f[i][k - 1] + f[k + 1][j] + w[i][j]}
即以k作为根节点,左子树的最小代价 + 右子树的最小代价 + 该区间的概率总和(因为产生了一个新的根节点,所有子节点和区间到当前根节点的距离都+1)
w[i][j] = w[i][j - 1] + p[j] + q[j]
即区间右端点每向右移动都把新增的节点和区间的概率加进来
初始值为:
因为单独的区间节点的最小代价就是自己本身,所以初始化
f[i][i - 1] = q[i - 1]
w[i][i - 1] = q[i - 1]
枚举方法为:

  1. 先遍历区间长度,1~n
  2. 遍历左节点1~n - len + 1,右节点为i + len - 1,len为1时左右节点为同一个点
  3. 分割区间,即选出使得代价最小的k作为根节点。。
    此时的代价为左右子树的代价和 + 根节点的代价
    根节点的代价为该节点的概率加上其叶节点概率和

最后因为该题算最底层区间节点到根节点的路径需要减1,所以需要减一遍区间节点的概率

代码如下:

#include <iostream>
#include <vector>
#include <limits.h>
using namespace std;

double optimalBST(int n, vector<double> p, vector<double> q){
    vector<vector<int>> root(n + 1, vector<int>(n + 1));
    vector<vector<double>> w(n + 2, vector<double>(n + 2)), e(n + 2, vector<double>(n + 2));
    double sum_q = 0;
    for(int i = 1; i < n + 2; i++){
        w[i][i - 1] = q[i - 1];
        e[i][i - 1] = q[i - 1];
    }
    for(int len = 1; len < n + 1; len++){
        for(int i = 1; i < n - len + 2; i++){
            int j = i + len - 1;
            e[i][j] = INT_MAX;
            w[i][j] = w[i][j - 1] + p[j] + q[j];//求该节点的概率加上其叶节点概率和
            for(int k = i; k < j + 1; k++){
                double temp = e[i][k - 1] + e[k + 1][j] + w[i][j];
				if (temp < e[i][j])
				{
					e[i][j] = temp;
					root[i][j] = k;
				}
            }
        }
    }
    for(auto i : q) sum_q += i;
    return e[1][n] - sum_q;
}

int main()
{
    int m;
    cin >> m;
    for(int i = 0; i < m; i++){
        int n;
        cin >> n;
        vector<double> value, p, q;//p为节点,q为区间
        double temp;
        for(int j = 0; j < n; j++){
            cin >> temp;
            value.push_back(temp);
        }
        for(int j = 0; j < n + 1; j++){
            if(j == 0){
                p.push_back(-1);
                continue;
            }
            cin >> temp;
            p.push_back(temp);
        }
        for(int j = 0; j < n + 1; j++){
            cin >> temp;
            q.push_back(temp);
        }
    printf("%.6lf\n", optimalBST(n, p, q));
    }
    return 0;
}

2.6.5 1026 插入乘号

题目:
在这里插入图片描述

思路:
区间模型

状态表示:
f[i][j],i使用了乘号的最后一个位置,j表示使用乘号的个数。即0i用了j个乘号,in - 1只使用了加法
sum[i][j],区间(i, j)数字的总和

状态方程:
f[i][j] = max{f[k][j - 1] * sum[k + 1][j]}
即(0, i)用了j个乘号 = (0, k)用了j - 1个乘号 * (k, i)所有元素的和

初始值:
f[i][0]=sum[1][i]
没有乘号时的情况(即第一列)可以直接用算好的sum初始化。

枚举方法
先填表sum,即区间 ( i , j ) (i,j) i,j中元素的和,这个表共 i ∗ j i * j ij个元素
再进行区间处理

  1. 遍历乘号个数j
  2. 遍历右节点i
  3. 遍历最后一个乘号的位置k,范围是当前乘号个数j和右节点i之间。
    d p [ k ] [ j − 1 ] ∗ s u m [ k + 1 ] [ i ] dp[k][j-1]*sum[k+1][i] dp[k][j1]sum[k+1][i]表示用完乘号的那一部分乘上余下数字之和。
    代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

long long dp[20][20];
int sum[20][20];
int a[20];

//dp[i][j]表示(1,i)中有j个乘号 dp[i][j]=max(dp[k][j-1]*sum[k+1][j]);(j<=k<i)

int main()

{
    int n,k,temp;

    cin >> temp;
    for(int num = 0; num < temp; num++)           
    {
        cin >> n >> k;
        for(int i=1;i<=n;i++)  scanf("%d",&a[i]);
        for(int i=1;i<=n;i++)
        {
            for(int j=i;j<=n;j++)
            {
                int cnt=0;
                for(int k=i;k<=j;k++) cnt+=a[k];
                sum[i][j]=cnt;
            }
        }
        for(int i=1;i<=n;i++) dp[i][0]=sum[1][i];
        for(int j=1;j<=k;j++)
        {
            for(int i=j+1;i<=n;i++)
            {
                dp[i][j]=-1;
                for(int k=j;k<i;k++)
                {
                    dp[i][j]=max(dp[i][j],dp[k][j-1]*sum[k+1][i]);
                }
            }
        }
        printf("%lld\n",dp[n][k]);

    }
    return 0;
}

2.6.6 1034 树上着色

题目:
在这里插入图片描述

思路:
树形模型,这道题相当于没有上司的舞会这道题
状态定义:
dp[u][0]用于表示当前节点不选时,res的最大值;dp[u][1]用于表示当前节点选时,res的最大值
状态方程(v为u的子节点):
u不选:dp[u][0] = ∑max(dp[v][0], dp[v][1]) v可选可不选
u选:dp[u][1] = ∑dp[v][0] v一定不能选

目标:max(dp[root][0], dp[root][1])

例子:
4
1 2
1 3
1 4

2 * 4的表
1 2 3 4
不选 3 0 0 0
选 0 1 1 1

//char **str 等价于 char *str[]
//char (*str)[20] 等价于 char [][20];
//当二维数组作为实参时,使用 char (*str)[20] 与 char [][20] 作为形参才是正确的用法。

代码如下:

#include<iostream>
#include <vector>
using namespace std;
#define MAXN 50005
long long f[MAXN][2];//表示每个节点可以选或者不选,填表

//char **str  等价于 char *str[]
//char  (*str)[20]  等价于 char [][20];
//当二维数组作为实参时,使用 char (*str)[20] 与 char [][20] 作为形参才是正确的用法。
void dp(int root, vector<int> son[MAXN]){
    f[root][0] = 0;
    f[root][1] = 1;
    for(int i = 0; i < son[root].size(); i++){
        int temp = son[root][i];
        dp(temp, son);//把子节点继续当成根节点进行遍历
        //选和不选两种情况都需要记录
        f[root][0] += max(f[temp][0], f[temp][1]);
        f[root][1] += f[temp][0];
    }
}

int main()
{
    int m;
    cin >> m;
    for(int i = 0; i < m; i++){
        int n;
        cin >> n;
        int v[MAXN];//标记该节点存在子节点
        vector<int> son[MAXN];//这里定义的是一个二维的vector
        for(int i = 1; i < n; i++){
            int x, y;
            cin >> x >> y;
            son[x].push_back(y);
            v[y] = 1;//如果有父节点则标记为1
        }
        int root;
        for(int i = 1; i < n + 1; i++){//找到根节点,即v中未被标记的那个点
            if(!v[i]){
                root = i;
                break;
            }
        }
        dp(root, son);
        printf("%lld\n",max(f[root][0], f[root][1]));

    }
    return 0;
}

采用邻接表的方法存储将该题的树当成一个无向图存储
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
add(a, b), add(b, a);
采用深度优先遍历遍历这个邻接表
每个节点的初始值为:
f [ u ] [ 0 ] = 0 f[u][0] = 0 f[u][0]=0
f [ u ] [ 1 ] = 1 f[u][1] = 1 f[u][1]=1
遍历到树的底部自底向上进行枚举,枚举的状态方程为:
f [ u ] [ 0 ] + = m a x ( f [ j ] [ 1 ] , f [ j ] [ 0 ] ) f[u][0] += max(f[j][1], f[j][0]) f[u][0]+=max(f[j][1],f[j][0])
f [ u ] [ 1 ] + = f [ j ] [ 0 ] f[u][1] += f[j][0] f[u][1]+=f[j][0]
用ans记录每一次枚举节点对应的最大值
a n s = m a x ( a n s , m a x ( f [ u ] [ 0 ] , f [ u ] [ 1 ] ) ) ans = max(ans, max(f[u][0], f[u][1])) ans=max(ans,max(f[u][0],f[u][1]))

#include<iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 50010;
int h[N], e[N * 2], ne[N * 2], idx;
bool st[N];
int n, dp[N][2], ans;

void add(int a, int b){
    //邻接表存边
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u){
    st[u] = true;
    dp[u][1] = 1;
    for(int i = h[u]; i != -1; i = ne[i]){
        //j当前节点值
        int j = e[i];
        //如果j遍历过了则跳过,避免向上查找
        if(!st[j]){
            dfs(j);
            //根据子节点来求根节点的两个状态
            //根节点如果涂色,则子节点一定不能涂
            //根节点如果不涂色,则子节点可涂可不涂
            dp[u][1] += dp[j][0];
            dp[u][0] += max(dp[j][0], dp[j][1]);            
        }
    }
    ans = max(ans, max(dp[u][1], dp[u][0]));
}

int main()
{
    int m;
    cin >> m;
    for(int i = 0; i < m; i++){
        memset(h, -1, sizeof(h));
        memset(dp, 0, sizeof(dp));
        memset(st, 0, sizeof(st));
        idx = 0, ans = 0;
        cin >> n;
        for(int j = 1; j < n; j++){
            int a, b;
            cin >> a >> b;
            add(a, b), add(b , a);
        }
        dfs(1);
        cout << ans << endl;

    }
    return 0;
}

2.6.7 1042 最低票价

题目:
在这里插入图片描述

思路:
0/1背包模型
dp问题的状态好像都是连续的
就像这题days不能只考虑去旅行的那几个,不然状态方程就没法写
填表不旅行的日子也需要填
这道题反过来思考比较好理解,就是通行证到期的那天付钱。而不是从前往后考虑,在开始使用的时候就付钱。

在填状态表时,每一个状态都看成旅行的最后一天。
这时可以是1天前,7天前,30天前购买的通行证,因为这样刚好用完,一定是花费最少的。

可以看成背包问题。旅行天数相当于背包的容量,costs相对于物品的价值。
当天旅行可看做装入容量为天数的物品,此时的价值为没装入时背包中的价值dp[i-w]加上该件物品的价值costs。

代码如下:

#include<iostream>
#include <vector>
using namespace std;

int dp(vector<int> days, vector<int> costs){
    int n = days.back();
    vector<int> dp(n + 1, 0);
    int a, b, c;
    for(int i = 0; i < days.size(); i++) dp[days[i]] = -1;
    for(int i = 1; i < n + 1; i++){
        if(dp[i] == 0) dp[i] = dp[i - 1];
        else{
            //买1天的通行证
            a = dp[i - 1] + costs[0];
            //买7天的通行证
            if(i - 7 < 0) b = costs[1];
            else b = dp[i - 7] + costs[1];
            //买30天的通行证
            if(i - 30 < 0) c = costs[2];
            else c = dp[i - 30] + costs[2];   
            dp[i] = min(a, b); 
            dp[i] = min(dp[i], c);        
        }
    }
    return dp[n];
}

int main()
{
    int m;
    cin >> m;
    for(int i = 0; i < m; i++){
        int n, temp;
        cin >> n;
        vector<int> days, costs;
        for(int i = 0; i < n; i++){
            cin >> temp;
            days.push_back(temp);
        }        
        for(int i = 0; i < 3; i++){
            cin >> temp;
            costs.push_back(temp);
        }

        cout << dp(days, costs) << endl;

    }
    return 0;
}

2.6.8 1021 钢条切割

题目:
在这里插入图片描述

思路:
0/1背包模型
和那个最低票价很像
每一钢条长度都可以看做是一个状态或者子问题
然后就是找这些子问题之间的联系
联系就是分割一块钢条(可以用0/1背包的思想,也就是装入一个物品)
当前长度的最大价值 = 除了上一块切分钢条之外长度对应的最大价值 + 上一块切分钢条的价格
即f[i - self.len[j]] + self.price[j]
代码如下:

class gangtiao:
    def __init__(self):
        self.length = 94
        self.num = 2
        self.len = [21, 88]
        self.price = [55, 64]

    def dp(self):
        f = [0] * self.length
        for i in range(self.length):
            temp = float('-inf')
            for j in range(len(self.len)):
                if i - self.len[j] >= 0:
                    temp = max(temp, f[i - self.len[j]] + self.price[j])
            if temp >= 0:
                f[i] = temp
        return f[self.length - 1]


if __name__ == "__main__":
    test = gangtiao()
    print(test.dp())

2.7 总结

常见的就四类问题,套模板就完事了,不过还是要多刷题,熟能生巧

3 贪心算法

3.1 基本思想

在求解某一问题时,每一步都做出当前看来是最好的决定。即不从整体考虑只考虑局部最优。

3.2 使用场景

  1. 局部最优可以导致全局最优
  2. 问题具有无后效性,即下一个状态只与当前状态有关,与之前的状态无关

3.3 基本步骤

  1. 建立数学模型来描述问题。

  2. 把求解的问题分成若干个子问题。

  3. 对每一子问题求解,得到子问题的局部最优解。

  4. 把子问题的解局部最优解合成原来解问题的一个解。

3.4 时间复杂度

3.5 经典问题

3.6 课程试题

3.6.1 1030 黑白连线

在这里插入图片描述

思路:
贪心问题。
要使得连线总长度最小,则让相近的黑白点优先连线。
用两个栈分别存放黑白两种点。
遇到黑点时查看白点栈是否为空,不为空则匹配;为空则放入黑点栈中。
每次匹配都计算当前总长度。
2n个点都遍历完则算法结束
代码如下:

#include<iostream>
#include <vector>
using namespace std;

int greedy(vector<int> points){
    int res = 0;
    vector<int> black, white;
    for(int i = 0; i < points.size(); i++){
        if(points[i] == 1){
            if(!white.empty()){
                res += (i - white.back());
                white.pop_back();
            }
            else{
                black.push_back(i);
            }
        }
        else{
            if(!black.empty()){
                res += (i - black.back());
                black.pop_back();
            }
            else{
                white.push_back(i);
            }            
        }
    }

    return res;
}

int main()
{
    int m;
    cin >> m;
    for(int i = 0; i < m; i++){
        int n, temp;
        cin >> n;
        vector<int> points;
        for(int i = 0; i < 2 * n; i++){
            cin >> temp;
            points.push_back(temp);
        }        

        cout << greedy(points) << endl;

    }
    return 0;
}

3.6.1 1031 基站布置

题目:
在这里插入图片描述

思路:

  1. 读取x,y并计算其在x轴上对应的圆心坐标,三者构成一个结构体
  2. 将基站按照在x轴上的横坐标排序
  3. 从左到右扫描,以最左边的船对应的圆心为基准,如果在园内则在cover中标记为1。(此步骤可优化,看代码)
  4. 扫描完后进入下一轮循环,基站数量加1,找到下一个cover中未被标记的点作为新一轮的圆心,重复3和4

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
struct pos_t {
    double x, y;
    double rsect;
} pos[10010];
int cover[10010];
int n;
double d;

const double minINF = 0.00000000001;//浮点误差

int cmp(const void *a, const void *b) {
    pos_t *ta, *tb;
    ta = (pos_t *)a;
    tb = (pos_t *)b;
    double temp = ta->rsect-tb->rsect;
    if(-minINF<=temp && temp<=minINF) {//浮点数比较注意预留一定的精度判断
    //if(temp == 0) {
        return 0;
    }
    else if (temp < 0) {
        return -1;
    }
    else {
        return 1;
    }
}

int solve() {
    scanf("%d%lf", &n, &d);
    for(int i=0; i<n; i++) {
        scanf("%lf%lf", &pos[i].x, &pos[i].y);
        pos[i].rsect = pos[i].x + sqrt(d*d-pos[i].y*pos[i].y);
    }
    memset(cover, 0, sizeof(cover));
    qsort(pos, n, sizeof(pos_t), cmp);
    int count = 0;
    for(int i=0; i<n; i++) {
        if(cover[i] == 1) {
            continue;
        }
        count = count + 1;
        for(int j=i; j<n; j++) {
            if(pos[j].rsect-pos[i].rsect > 2*d) {
                break;
            }
            if(cover[j]==1) {
                continue;
            }
            //下面也需要注意浮点误差
            double temp = (pos[j].x-pos[i].rsect)*(pos[j].x-pos[i].rsect) + pos[j].y*pos[j].y - d*d;
            if(temp<=minINF) {
                cover[j] = 1;
            }
        }
    }
    printf("%d\n", count);
}

int main() {
    int m;
    scanf("%d", &m);
    for(int i=0; i<m; i++) {
        solve();
    }
    return 0;
}

3.6.1 1033 机器作业

题目:
在这里插入图片描述

思路:
不知道对错,样例没问题,但提交没有过(要用longlong类型才给过…)

  1. 按照收益排序
  2. 创建一个时间窗口,按照收益的顺序,依次从其ddl往前遍历到一个空位置放入
    代码如下:
#include<iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct work{
    long long d;//deadline
    long long p;//profit
};

bool cmp(work a, work b){
    if(a.p > b.p) return 1;
    return 0;
}

long long greedy(vector<work> works){
    bool time[works.size()];
    for(int i = 0; i < works.size(); i++) time[i] = 0;
    long long res = 0;
    for(auto i : works){
        for(int j = i.d - 1; j >= 0; j--){
            if(time[j] == 0){
                time[j] = 1;
                res += i.p;
                break;
            }
        }
    }

    return res;
}

int main()
{
    int m;
    cin >> m;
    for(int i = 0; i < m; i++){
        int n;
        long long temp;
        cin >> n;
        vector<work> works;
        work temp_work;
        for(int i = 0; i < n; i++){
            cin >> temp;
            temp_work.d = temp;
            cin >> temp;
            temp_work.p = temp;
            works.push_back(temp_work);
        }        
        sort(works.begin(), works.end(), cmp);
        printf("%lld\n", greedy(works));

    }
    return 0;
}

3.7 总结

感觉贪心算法挺玄学的,还需要多悟一悟…

总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值