前言
第一学期算法课,刷题总结
http://47.99.179.148/
1 分治法
1.1 基本思想
将一个难以解决的大问题分割为若干个规模较小的相同子问题。
由于子问题与原问题的处理方法相同,所以可以采用递归的方法来解决。
1.2 使用场景
- 子问题易解决
- 最优子结构性质:可以分为若干个规模较小的相同子问题。
- 分解的子问题可以再合并为该问题的解。如果满足1和2,不满足3可以考虑贪心和动态规划。
- 子问题相互独立:如果子问题中有交集分治法的效率就会下降,因为要考虑到合并时如何处理公共部分。所以这时候使用动态规划较好。
1.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 经典问题
- 二分搜索
- 大整数乘法
- Strassen矩阵乘法
- 棋盘覆盖
- 合并排序
- 快速排序
- 线性时间选择
- 最接近点对问题
- 循环赛日程表
- 汉诺塔
1.6 课程试题
1.6.1 1004 归并排序
题目:
思路:
- 先确定分割点,一般是二分或者随机分割。这道题是二分。
- 对左半部分和右半部分分别进行递归,即继续分割。分割其实就是向下传递处理好的数组下标。
- 递归到不能再分割之后,进行子问题的处理以及合并。处理即将分割的两个部分进行一起排序。
分别创建两个新数组存放左右两部分。
然后依次遍历这两个数组,相比较每次选出较小的元素放入原数组的相应位置。
最后将没有遍历完的数组放在原数组的余下位置。
注意这两个数组也是上一层递归而来的,所以也是有序的。
代码如下:
#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
- 分割点,m = l + r >> 1
- 分割函数,一般就是递归这个函数
- 合并函数,用于处理合并子问题
最后实现处理子问题的函数merge
2 动态规划
2.1 基本思想
和分治法类似,都是将复杂问题分割为若干个子问题。
不过动态规划的子问题为纵向的子问题,可以使用前一子问题的信息来解决后一子问题,从而避免重复计算,降低了时间复杂度。
2.2 使用场景
- 子问题存在交集且为纵向
- 当前状态(子问题)可能与之前的好几个状态(子问题)存在关联
- 存在最优子结构性质
知乎某网友:状态转移树中,若后一状态仅仅取决于上一个状态,就用贪婪算法;若后一状态取决于之前的多个状态,就用动态规划。
2.3 基本步骤
- 自顶向下(备忘录)
自顶向下递归,建立一个数组(备忘录/哈希表)来记录之前计算过的值
每一次计算前检查备忘录,如果算过了就可以拿来直接用 - 自底向上
自底向上计算,也就是一个填表的过程。表中的每一个位置都看做一个独立的状态(子问题)来解决
解决每个子问题可以用到之前解决过的子问题的结果
2.4 时间复杂度
…
2.5 经典问题
自顶向下(备忘录)
斐波那契数列的计算
自底向上
- 线性模型(这个我不太熟)
- 0/1背包模型
- 区间模型
- 树形模型
2.6 课程试题
2.6.1 1009 拦截导弹1
题目:
思路:
0/1背包模型
状态为能够最多拦截导弹的数量
在处理第i个导弹时,扫描之前的i - 1个导弹
如果之前发射的导弹j高度小于第i个导弹则可以
- 选择发射该导弹,当前发射的导弹数为dp[j] + 1
- 不发射,当前导弹数为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[i−1][v],f[i−1][v−ci]+Wi}
如果要使得背包恰好装满,则其对应的子问题(子状态/容量更小的背包)也必须要恰好装满。
所以初始化第一行第一个元素为0,其他都为无穷小。
这样从第一次开始放入物品,若不是恰好装满,则物品价值加上无穷小还是为无穷小。所以最后判断如果dp[n][n]不为无穷小则说明可以全部装满。
比如,如果背包容量为4,这时候装第一个物品容量为3,则上一个状态为
f
[
0
]
[
4
−
3
]
f[0][4 - 3]
f[0][4−3]为负无穷小,
f
[
0
]
[
4
−
3
]
+
v
[
1
]
f[0][4 - 3] + v[1]
f[0][4−3]+v[1]还是负无穷小
如果装入第一个物品容量为4,则上一个状态为
f
[
0
]
[
4
−
3
]
f[0][4 - 3]
f[0][4−3]为0,
f
[
0
]
[
4
−
3
]
+
v
[
1
]
f[0][4 - 3] + v[1]
f[0][4−3]+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 [i−1]∗ weight [k]∗ weight [j])
- 第一个for枚举区间长度。最少2个矩阵,最多n个。
- 第二个for枚举区间左端点。左端点从0开始枚举,最大可为n - 1 - len。
- 第三个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 最优二叉搜索树
题目:
思路:
区间模型
题意:
- 关键字和关键字外区间(共N个节点和N+1个区间)也有搜索概率;
- 关键字节点和关键字之外区间的搜索代价:该节点(区间)到树的根的路径上关键字节点的个数;
- 期望代价为所有关键字和关键字之外区间的期望代价之和
网上的题解在处理区间节点时路径算的是该区间节点到根节点;
而这道题处理区间节点时路径算的是其上一个关键字节点到根节点;
等于网上的题解针对这道题是多算了一遍区间节点,最后减掉即可
代码思路:
需要构建两个二维数组来存储区间的一些信息
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~n
- 遍历左节点1~n - len + 1,右节点为i + len - 1,len为1时左右节点为同一个点
- 分割区间,即选出使得代价最小的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
i∗j个元素
再进行区间处理
- 遍历乘号个数j
- 遍历右节点i
- 遍历最后一个乘号的位置k,范围是当前乘号个数j和右节点i之间。
d p [ k ] [ j − 1 ] ∗ s u m [ k + 1 ] [ i ] dp[k][j-1]*sum[k+1][i] dp[k][j−1]∗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 使用场景
- 局部最优可以导致全局最优
- 问题具有无后效性,即下一个状态只与当前状态有关,与之前的状态无关
3.3 基本步骤
-
建立数学模型来描述问题。
-
把求解的问题分成若干个子问题。
-
对每一子问题求解,得到子问题的局部最优解。
-
把子问题的解局部最优解合成原来解问题的一个解。
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 基站布置
题目:
思路:
- 读取x,y并计算其在x轴上对应的圆心坐标,三者构成一个结构体
- 将基站按照在x轴上的横坐标排序
- 从左到右扫描,以最左边的船对应的圆心为基准,如果在园内则在cover中标记为1。(此步骤可优化,看代码)
- 扫描完后进入下一轮循环,基站数量加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类型才给过…)
- 按照收益排序
- 创建一个时间窗口,按照收益的顺序,依次从其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提供了大量能使我们快速便捷地处理数据的函数和方法。