WHUT第七周训练整理
祝各位武理的ACMer们,新年快乐!在接下来的一年里继续训练,为武理的ACM揭开新的篇章!同为武汉的学子,武汉加油!
索引
(难度由题目自身难度与本周做题情况进行分类,仅供新生参考!)
零、动态规划与贪心算法的区别
一、easy:01、02、03、06、11、15
二、medium:04、05、07、08、09、10、12、13、14、16、17、18、20
三、hard:19、21、22
本题解报告大部分使用的是C++语言,在必要的地方使用C语言解释。
零、动态规划与贪心算法的区别
贪心算法(Greedy algorithm): 从问题解题过程一步一步按照某个策略取当前的最优值,并且达到全局最优结果;
动态规划(DP):在解决当前问题的时候考虑到之前所有的结果使得过去最优。因此动态规划得到的是每时每刻的全局最优解,并且逐步扩大问题规模。动态规划的精髓是组合子问题。
从方法上来说,
贪心算法需要严格证明全局正确性,并合理设计当前策略;
动态规划算法需要设计状态转移方程,考虑到状态转移的所有情况。
从效率上来说,贪心算法在一般情况下快于动态规划——当然前提是两种算法都能得到正确的结果。
作者:知乎用户
扫描二维码关注公众号,回复: 9254986 查看本文章链接:https://www.zhihu.com/question/36662980/answer/68494301
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
一、easy
1001:Bone Collector(01背包)
题意:现在有 根骨头还有一个容量为 的背包,每根骨头都有自身的重量以及价值,问怎么选择骨头放入背包在能获得最大价值呢?
范围:
分析:经典的背包背景,也是 的入门题,即 背包,每个物品只有取和不取两种状态,对应 和 。
接下来我们来考虑怎么设计,首先影响答案的因素有哪些?
- 可以选择的物品
- 剩余背包的容量
那么设 表示可以在 物品中进行选择,且可用容量为 时能够取到的最大价值。
转移方程:
-
如果当前的可用容量 >= 物品i的重量,那么
( 表示不选择物品 ,在 物品中进行选择的最优解; 表示选择物品 ,还有剩余的容量在 物品中进行选择的最优解)
-
如果当前的可用容量 < 物品 的重量,那么
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000 + 10;
int n, v;
int dp[MAXN][MAXN];
int value[MAXN], weight[MAXN]; // 物品的价值与重量
int main()
{
int T;
cin >> T;
while (T--)
{
memset(dp, 0, sizeof(dp)); // 多组输入注意清空数组
cin >> n >> v;
for (int i = 1; i <= n; i++)
{
cin >> value[i];
}
for (int i = 1; i <= n; i++)
{
cin >> weight[i];
}
// 两种转移方程
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= v; j++)
{
if (j < weight[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[n][v] << endl;
}
return 0;
}
1002:Super Jumping! Jumping! Jumping!(dp)
题意:给 个数字 ,可以从任意位置开始从左往右跳,但是只能跳到数字比当前数字大的位置,到达位置上的数字都可以收集起来,问最大能够收集多少呢?
范围:
分析:没想到可以证明正确性的贪心策略,那么考虑 。
影响当前状态的因素有哪些?
- 选择区间
- 终点位置
设 表示可以在区间 中进行选择并且以第 个数字为终点的最优解。因为起点是任意的,所以 的初值就是自身 。
转移方程:
当 且
(满足路径上的数字是严格递增的)
时间复杂度是 ,可以接受。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000 + 10;
int n;
int arr[MAXN], dp[MAXN];
int main()
{
while (cin >> n, n)
{
memset(dp, 0, sizeof(dp)); // 多组输入注意清空数组
for (int i = 1; i <= n; i++)
{
cin >> arr[i];
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
dp[i] = arr[i]; // 初始值就是自身
// 寻找前面数字比自己小的地方进行比较更新
for (int j = 1; j < i; j++)
{
if (arr[j] < arr[i])
dp[i] = max(dp[i], dp[j] + arr[i]);
}
ans = max(ans, dp[i]); // 最优解的终点在1~n之间,故取最大值
}
cout << ans << endl;
}
return 0;
}
1003:命运(dp)
题意:给一个 的二维矩阵,现在位于 ,要到 。若当前位置为 ,那么可以移动到 这些位置,其中 ,并且将到达位置上的数字 (可正可负)收集起来,问到达 时收集的最大值是多少?
范围:
分析:没想到可以证明正确性的贪心算法,考虑使用 。
此类问题的状态转移还是比较容易想到的,考虑点 ,只能是从 这些点转移来, 满足 。
影响因素有哪些?
- 可供使用的行区间
- 可供使用的列区间
设 表示到达位置 时的最优解。
转移方程:
当
当
当
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 20 + 10;
const int MAXM = 1000 + 10;
int n, m;
int arr[MAXN][MAXM], dp[MAXN][MAXM];
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
cin >> arr[i][j];
dp[i][j] = -200; // 这里-200代表的是极小值
}
}
dp[1][1] = arr[1][1]; // 第一个点不能通过转移得到,所以先预处理
// 根据三个转移方程进行计算
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
if (i == 1 && j == 1)
continue;
if (i - 1 >= 1)
dp[i][j] = max(dp[i][j], dp[i - 1][j]);
if (j - 1 >= 1)
dp[i][j] = max(dp[i][j], dp[i][j - 1]);
for (int k = 2; k <= j; k++) // 注意 k > 1
{
if (j % k == 0)
{
dp[i][j] = max(dp[i][j], dp[i][j / k]);
}
}
dp[i][j] += arr[i][j];
}
}
cout << dp[n][m] << endl;
}
return 0;
}
1006:数塔(dp)
题意:给一个数塔,每个格子都有数字 ,现在要求从顶层到达底层能够获得的最大数字之和。
范围:
分析:没想到可以证明正确性的贪心算法,考虑使用 。
影响因素有哪些?
- 到达左上方的最大数字之和
- 到达右上方的最大数字之和
设 表示到达位置 时的最大数字之和,那么答案就是 中的最大值。
转移方程:
当
当 且
(需要保证 和 这两个点是存在的)
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100 + 10;
int n;
int arr[MAXN][MAXN], dp[MAXN][MAXN];
int main()
{
int T;
cin >> T;
while (T--)
{
// 初始化
memset(arr, -1, sizeof(arr));
memset(dp, 0, sizeof(dp));
cin >> n;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
cin >> arr[i][j];
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
// 右上方数字存在
if (arr[i - 1][j] != -1)
dp[i][j] = max(dp[i][j], dp[i - 1][j]);
// 左上方数字存在
if (arr[i - 1][j - 1] != -1)
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1]);
dp[i][j] += arr[i][j]; // 加上当前数字
}
}
// 取最后一层的最大值
int ans = 0;
for (int i = 1; i <= n; i++)
{
ans = max(ans, dp[n][i]);
}
cout << ans << endl;
}
return 0;
}
1011:最少拦截系统(dp/贪心)
不说了,之前第四周的贪心场训练已经做过了,可以参考我第四周的题解报告 。
1015:Doing Homework again(贪心/dp)
同样第四周贪心场已经做过了,可以去看看。
二、medium
1004:Monkey and Banana(拆点+dp)
题意:给 种长方体,每种长方体数量不限,现在要将长方体垒起来,要保证上面长方体的底面面积严格小于下面长方体的顶面面积。现在问最高能够垒到多高?
范围:
分析:因为需要同时考虑一个面的长度和宽度,不好贪心,因此考虑使用 。
首先对于一个长方体 ,其底面的尺寸有三种情况: 以及 ,所以把一个长方体拆分成三个面来考虑。
我们可以考虑在第 层的时候选择 的面,那么下面紧贴着这个面的选择可能有很多种,我们只要求出那些可以选择的面中的最优解就可以了,而且这些底下的面可以事先以同样的方式求得。
影响因素有哪些?
- 当前层数
- 当前层使用的是第几个面
设 表示第 层选择第 个面作为顶层的最优解。
但是最多会有多少层呢?可以推算出任意一个块自己叠在自己上面,至多只能叠加一次。那么我们可以知道在 最大为 的情况,最多有 个面,每个面最多叠加一次,因此最多有 层。
转移方程:
这里 是满足条件的面序号, 是当前长方体的高度
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 30 + 10;
int n;
int arr[MAXN * 3][3], dp[MAXN * 6][MAXN * 3]; // 有3n个面,有6n层
int main()
{
int kase = 1;
while (cin >> n, n)
{
memset(dp, -1, sizeof(dp)); // 初始化
int index = 1;
for (int i = 0; i < n; i++)
{
int l, w, h;
cin >> l >> w >> h;
// 分别保存三个面,[2]中保存高度
arr[index][0] = l, arr[index][1] = w, arr[index++][2] = h;
arr[index][0] = l, arr[index][1] = h, arr[index++][2] = w;
arr[index][0] = w, arr[index][1] = h, arr[index++][2] = l;
}
// 第一层预处理,高度可以直接确定
for (int i = 1; i < index; i++)
{
dp[1][i] = arr[i][2];
}
// 其他层按照转移方程计算
for (int i = 2; i <= 200; i++)
{
for (int j = 1; j < index; j++)
{
for (int k = 1; k < index; k++)
{
if (dp[i - 1][k] == -1)
continue;
int w1 = arr[j][0], w2 = arr[k][0], l1 = arr[j][1], l2 = arr[k][1];
// 满足条件
if ((w1 < w2 && l1 < l2) || (w1 < l2 && l1 < w2))
{
dp[i][j] = max(dp[i][j], dp[i - 1][k] + arr[j][2]);
}
}
}
}
int ans = 0;
// 取所有层中选择所有面作为顶面的最优解
for (int i = 1; i <= 200; i++)
{
for (int j = 1; j < index; j++)
{
ans = max(ans, dp[i][j]);
}
}
cout << "Case " << kase++ << ": maximum height = " << ans << endl;
}
return 0;
}
1005:Big Event in HDU(思维+01背包)
题意:有 类物品,都有各自的价值 ,每类物品有 个,现在需要将这些物品分成两堆,让两堆的总价值相差最小。
范围:
分析:这道题目还是比较套路的,设所有物品的价值之和为 ,那么要让两堆物品的总价值相差最小,相当于两堆的总价值尽量往 靠近,因此我们可以在 的背包容量内做 背包!
(唉,之前西安邀请赛的时候就出了个这样的题目,那时候没想出来是背包,要是我早点做了这题就好了,大家还是要好好训练啊!)
这道题目中这些物品的数量可以有多个,总数量不超过 个,可以把物品拆分出来,用 背包,但是注意空间需要优化一下,我这里使用两行的 数组,因为确定 只需要上一层的 值。(其实是可以优化成一维的,后面的题目会用到)
详见代码。
Code:
// 这题直接用cin会TLE,所以我改成了scanf,取消流同步应该也可以
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5000 + 10;
int n;
int value[MAXN], dp[2][MAXN*25]; // 只用两行,背包的容量最大为50*50*100/2
int main()
{
while (~scanf("%d", &n), n > 0)
{
memset(dp, 0, sizeof(dp)); // 清空数组
int sum = 0; // 总价值
int index = 1; // 用于拆分物品
for (int i = 1; i <= n; i++)
{
int v, c;
scanf("%d%d", &v, &c);
sum += v * c;
// 拆分
for (int j = 0; j < c; j++)
{
value[index++] = v;
}
}
// 01背包
for (int i = 1; i < index; i++)
{
for (int j = 0; j <= sum / 2; j++)
{
// 因为只用两行,所以根据奇偶性判断上一层
if (j >= value[i])
{
dp[i % 2][j] = max(value[i] + dp[!(i % 2)][j - value[i]], dp[!(i % 2)][j]);
}
else
{
dp[i % 2][j] = dp[!(i % 2)][j];
}
}
}
// 保证左边不比右边小
int ans1 = max(dp[0][sum / 2], dp[1][sum / 2]);
int ans2 = sum - ans1;
if (ans1 < ans2)
swap(ans1, ans2);
printf("%d %d\n", ans1, ans2);
}
return 0;
}
1007:免费馅饼(dp)
题意:天上掉 个馅饼,分别在 时刻掉落在区间 之间,一开始人在 ,每秒钟可以往左或者往右走一格,或者待在原地。现在问最多能接到多少馅饼?
范围:
分析:没想到可以证明的贪心策略,考虑 。
考虑在时间为 的时候当前位置在 的情况是怎么转移来的,只能是在 时刻从 这三个位置过来的。
影响因素有哪些?
- 上一秒位置在 时的最优解
- 上一秒位置在 时的最优解
- 上一秒位置在 时的最优解
设 表示时刻 在位置 的最优解,答案就是 时刻 个位置中的最大值。
另外我们使用数组 表示时刻 在位置 时有多少个饼掉下,同一时刻同一位置可能有多个饼。
转移方程:
(上一秒附近三个位置的最大值+该时刻该位置的馅饼掉落数量)
Code:
// 提示:本题的输入数据量比较大,建议用scanf读入,用cin可能会超时。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10;
int n;
int arr[MAXN][12], dp[MAXN][12];
int main()
{
while (~scanf("%d", &n), n)
{
// 清空数组
memset(arr, 0, sizeof(arr));
memset(dp, 0, sizeof(dp));
int T = 0, ans = 0;
for (int i = 0; i < n; i++)
{
int t, x;
scanf("%d%d", &x, &t);
arr[t][x]++;
T = max(T, t); // 有效时间的上限
}
// 初始化
dp[1][4] = arr[1][4];
dp[1][5] = arr[1][5];
dp[1][6] = arr[1][6];
// 根据转移方程计算
for (int i = 2; i <= T; i++)
{
for (int j = 0; j <= 10; j++)
{
dp[i][j] = max(dp[i - 1][j - 1], max(dp[i - 1][j], dp[i - 1][j + 1])) + arr[i][j];
}
}
// 取所有点上的最大值
for (int i = 0; i <= 10; i++)
{
ans = max(ans, dp[T][i]);
}
printf("%d\n", ans);
}
return 0;
}
1008:I NEED A OFFER!(数学+dp)
题意:身上有 万美元,现在有 个学校可以申请,申请需要花费 万元,拿到offer的概率为 ,问怎么安排至少能拿到一个offer。
范围: 整型 实型
分析: 背包的变形,需要进行一些转换。
本题要我们求至少拿到一个offer的最大概率,那么意思就是说要求一个offer都拿不到的最小概率。
而 背包处理的时候在容量为 的情况下最多能取到的价值,我们这里需要资金为 的情况下取到的最小概率,其实是差不多的。
设 表示可以选择 学校并且资金为 时能够取到的最小概率,注意需要优化空间,不然数组开不下。
转移方程: 数组全部初始化为
当
当
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e4 + 10;
int n, m;
double dp[2][MAXN]; // 采用两行dp
int cost[MAXN]; // 申请的花费
double chance[MAXN]; // 拿到offer的概率
int main()
{
while (cin >> n >> m, n + m)
{
// 初始化为1
for (int i = 0; i <= 1; i++)
{
for (int j = 0; j <= n; j++)
{
dp[i][j] = 1;
}
}
for (int i = 1; i <= m; i++)
{
cin >> cost[i] >> chance[i];
}
// 01背包变形
for (int i = 1; i <= m; i++)
{
for (int j = 0; j <= n; j++)
{
if (j >= cost[i])
{
// 根据奇偶性判断上一层dp值
dp[i % 2][j] = min(dp[!(i % 2)][j], dp[!(i % 2)][j - cost[i]] * (1 - chance[i]));
}
else
{
dp[i % 2][j] = dp[!(i % 2)][j];
}
}
}
// 1-拿不到offer的最小概率=拿到至少一个offer的最大概率
double ans = 1 - dp[m % 2][n];
cout << fixed << setprecision(1) << ans * 100 << "%" << endl;
}
return 0;
}
1009:FATE(三维完全背包)
题意:升级需要 点经验,还剩下 点忍耐度,有 种怪物,数量无限,杀掉每种怪物能得到 点经验并且花费 点忍耐度,现在问是否能够在杀不超过 个怪物的条件下升级,如果能则输出升级后还能保留的最大忍耐度,否则输出 。
范围:
分析:又是背包的变形,看到怪物的数量是无限的,那么应该想到是完全背包,不过多了一个维度变成三维,稍微改一点就可以。
除此以外,如果能够升级的话我们还是需要输出能保留的最大忍耐度,这个可以在我们求完 数组之后遍历背包容量 ,一旦当前最大经验值大于所需要的经验值,那么剩下的忍耐度就是答案。
影响的因素有哪些?
- 选择区间
- 剩余背包容量
- 剩余能杀的怪物数量
设 表示在 物品中选择、当前的容量为 并且还能选 个物品的最优解。
转移方程:
当 并且
其他情况
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100 + 10;
int n, m, k, s;
int dp[MAXN][MAXN][MAXN];
int cost[MAXN], value[MAXN];
int main()
{
while (cin >> n >> m >> k >> s)
{
memset(dp, 0, sizeof(dp)); // 清空数组
for (int i = 1; i <= k; i++)
{
cin >> value[i] >> cost[i];
}
// 三维完全背包
for (int i = 1; i <= k; i++)
{
for (int j = 0; j <= m; j++)
{
for (int t = 0; t <= s; t++)
{
// 保证容量足够并且还可以杀怪
if (j >= cost[i] && t)
{
dp[i][j][t] = max(dp[i - 1][j][t], dp[i][j - cost[i]][t - 1] + value[i]);
}
else
{
dp[i][j][t] = dp[i - 1][j][t];
}
}
}
}
// 先判断能不能升级
if (dp[k][m][s] < n)
{
cout << -1 << endl;
continue;
}
// 再从0~m遍历背包得到最大剩余的忍耐度
int ans = 0;
for (int i = 0; i <= m; i++)
{
if (dp[k][i][s] >= n)
{
ans = m - i;
break;
}
}
cout << ans << endl;
}
return 0;
}
1010:Coins(多重背包)
题意:钱包中有 种硬币,面值为 的有 个,现在问通过组合能够得到多少个不大于 的金额。
范围:
分析:仍然是背包的背景,这题是多重背包模板题,可以学习一下。
按我的理解多重背包就是介于 背包与完全背包之间的背包问题,有 个物品,每个物品都有自己的价值还有重量,除此以外还有数量的限制,这个就是最主要的不同之处了。
背包每种物品只有1个,完全背包每种物品有无数个。
经过处理之后多重背包能够转换成 背包和完全背包。
假设一个物品的数量限制为 ,那么我们可以选择这个物品 个。
分类讨论:
- 若 (背包总容量),那么在这种情况下实际上是供大于求的,可以把这个数量看作是无限个,那么问题就转化成完全背包了。
- 否则,我们可以按照二进制将这
个物品拆分成
个,那么无论最终这个物品选择了多少次,我们都可以用若干个拆分出来的物品组合而成。
为什么?因为我们选择的物品数量 一定不大于 ,且 可以用二进制表示,而其中二进制每一位上的数子我们都已经单独拆分出来了,可以直接进行组合,那么问题转化成 背包了。
Notice:本题不得不使用优化成一维数组的背包了!
若使用一维数组,那么对于 背包来说,需要从容量大往小进行更新 数组,此时数组左边的部分就是相当于上一层的部分,右边的部分就是本层的部分;对于完全背包来说,因为完全背包需要利用本层更新的 数组,因此需要从容量小往大进行更新 数组。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100 + 10;
const int MAXM = 100000 + 10;
const int INF = 0x3f3f3f3f;
int n, m;
int value[MAXN], cnt[MAXN];
int dp[MAXM]; // 一维数组!
// 01背包
void zeroOnePack(int weight, int value)
{
for (int i = m; i >= weight; i--) // 01背包的一维从右往左,使用上层的dp
{
dp[i] = max(dp[i], dp[i - weight] + value);
}
}
// 完全背包
void completePack(int weight, int value)
{
for (int i = weight; i <= m; i++) // 完全背包的一维从左往右,使用本层的dp
{
dp[i] = max(dp[i], dp[i - weight] + value);
}
}
// 多重背包
void multiplePack(int weight, int value, int number)
{
// 供大于求,转化成完全背包
if (m <= number * weight)
{
completePack(weight, value);
}
// 否则拆分物品,转化成01背包
else
{
// 将0~number二进制每一位上的1拆分出来
int k = 1;
while (k <= number)
{
zeroOnePack(k * weight, k * value);
number -= k;
k *= 2;
}
zeroOnePack(number * weight, number * value);
}
}
int main()
{
while (cin >> n >> m, n + m)
{
for (int i = 1; i <= n; i++)
{
cin >> value[i];
}
for (int i = 1; i <= n; i++)
{
cin >> cnt[i];
}
// 初始化数组
for (int i = 0; i <= m; i++)
{
dp[i] = -INF;
}
// 容量为0的dp数组需要置为0
dp[0] = 0;
// 多重背包
for (int i = 1; i <= n; i++)
{
multiplePack(value[i], value[i], cnt[i]);
}
// 统计答案
int ans = 0;
for (int i = 1; i <= m; i++)
{
if (dp[i] > 0)
ans++;
}
cout << ans << endl;
}
return 0;
}
1012:Common Subsequence(dp)
题意:给两个字符串 ,求两者的最长公共子序列长度。
范围:没有明确指出,估计都是少于 的
分析:经典的 问题,必须掌握。
影响的因素有哪些?
- 字符串 的指针位置
- 字符串 的指针位置
设 表示 当前匹配位置为 , 当前匹配位置为 时的最长公共子序列。
如果 的话,那么该元素一定是位于两者的 中的,因此只需要在剩下的串中求 即可;
否则,那么剩下串的 只能在 与 中产生。
转移方程:
当 ==
其他情况
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000 + 10;
const int INF = 0x3f3f3f3f;
int n;
int dp[MAXN][MAXN];
int LCS(string str1, string str2)
{
int m = str1.length(), n = str2.length();
int i, j;
dp[0][0] = 0;
for (i = 1; i <= m; ++i)
{
dp[i][0] = 0;
}
for (i = 1; i <= n; ++i)
{
dp[0][i] = 0;
}
for (i = 1; i <= m; ++i)
{
for (j = 1; j <= n; ++j)
{
if (str1[i - 1] == str2[j - 1])
{
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else if (dp[i - 1][j] > dp[i][j - 1])
{
dp[i][j] = dp[i - 1][j];
}
else
{
dp[i][j] = dp[i][j - 1];
}
}
}
return dp[m][n];
}
int main()
{
string str1, str2;
while (cin >> str1 >> str2)
{
cout << LCS(str1, str2) << endl;
}
return 0;
}
1013:搬寝室(贪心+dp)
题意:有 件物品,重量为 ,只需要搬其中 件,每次需要选择 件物品,即需要搬 趟,每次搬运的花费是这两件物品重量差的平方,即 。问至少需要花费多少的代价才能搬完 件物品。
范围:
分析:一开始确实感觉有点无从下手,但是观察发现每次选择两个物品的时候,一定要选择重量相邻的两个物品,否则不可能是最优解。
比如有重量为 的四个物品,重量递增,如果选了 ,要么选 或者选 ,不能选 。
因此我们要先把物品按照重量进行排序,这样对我们接下来的 有帮助。
影响的因素有哪些?
- 物品选择区间
- 还需要选择的物品数量
设 表示处理 这些物品并且需要选 次物品时的最优解
转移方程:
(不选择和选择 和 这一对物品两种情况中的最优解)
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2000 + 10;
const int INF = 0x3f3f3f3f;
int n, k;
int arr[MAXN], dp[MAXN][MAXN / 2];
int main()
{
while (cin >> n >> k)
{
// 初始化
dp[0][0] = 0;
for (int i = 0; i <= n; i++)
{
for (int j = 1; j <= k; j++)
{
dp[i][j] = INF; // dp[i][j]的初始值要够大,不然会wa的
}
}
for (int i = 1; i <= n; i++)
{
cin >> arr[i];
}
sort(arr + 1, arr + 1 + n); // 先排序
// 根据转移方程更新dp数组
for (int i = 2; i <= n; i++)
{
for (int j = 1; j <= k; j++)
{
dp[i][j] = min(dp[i - 1][j], dp[i - 2][j - 1] + (arr[i] - arr[i - 1]) * (arr[i] - arr[i - 1]));
}
}
cout << dp[n][k] << endl;
}
return 0;
}
1014:Humble Numbers(数学+dp预处理)
题意:输出第 个丑数。
范围:
分析:任何一个数分解到最后都是由一些质数或者 相乘。基于此,题目中设一个数的质数因子在 之中,所以,这个数应当从 开始,不断乘以 获得。
因此我们可以从 开始预处理出所有的丑数,直接输出答案即可。
详见代码。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 6000 + 10;
int n;
int ans[MAXN];
int main()
{
// dp预处理
int index2 = 1, index3 = 1, index5 = 1, index7 = 1;
ans[1] = 1;
for (int i = 2; i <= 6000; i++)
{
ans[i] = min(2 * ans[index2], min(3 * ans[index3], min(5 * ans[index5], 7 * ans[index7])));
// 4个if,如果出现重复的可以跳过
// 比如6,可以被2和3整除,只保留一个,都往后走
if (ans[i] == 2 * ans[index2])
index2++;
if (ans[i] == 3 * ans[index3])
index3++;
if (ans[i] == 5 * ans[index5])
index5++;
if (ans[i] == 7 * ans[index7])
index7++;
}
while (cin >> n, n)
{
cout << "The " << n;
// 其实只要知道前13个数字就可以了,下面是我的写法
if(n%10 == 1 && n%100 != 11) cout << "st";
else if(n%10 == 2 && n%100 != 12) cout << "nd";
else if(n%10 == 3 && n%100 != 13) cout << "rd";
else cout << "th";
cout << " humble number is " << ans[n] << "." << endl;
}
return 0;
}
1016:悼念512汶川大地震遇难同胞——珍惜现在,感恩生活(多重背包)
题意:给 元钱去买米,市场有 种米,每种米价格、重量以及数量分别为 ,现在问最多能买多少斤的米。
范围:
分析:多重背包模板题,与 类似。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100 + 10;
const int MAXM = 100000 + 10;
const int INF = 0x3f3f3f3f;
int n, m;
int cost[MAXN], value[MAXN], cnt[MAXN];
int dp[MAXM];
void zeroOnePack(int weight, int value)
{
for (int i = m; i >= weight; i--) // 01背包的一维从右往左,使用上层的dp
{
dp[i] = max(dp[i], dp[i - weight] + value);
}
}
void completePack(int weight, int value)
{
for (int i = weight; i <= m; i++) // 完全背包的一维从左往右,使用本层的dp
{
dp[i] = max(dp[i], dp[i - weight] + value);
}
}
void multiplePack(int weight, int value, int number)
{
if (m <= number * weight)
{
completePack(weight, value);
}
else
{
int k = 1;
while (k <= number)
{
zeroOnePack(k * weight, k * value);
number -= k;
k *= 2;
}
zeroOnePack(number * weight, number * value);
}
}
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> m >> n;
for (int i = 1; i <= n; i++)
{
cin >> cost[i] >> value[i] >> cnt[i];
}
for (int i = 0; i <= m; i++)
{
dp[i] = -INF;
}
dp[0] = 0;
for (int i = 1; i <= n; i++)
{
multiplePack(cost[i], value[i], cnt[i]);
}
cout << dp[m] << endl;
}
return 0;
}
1017:Piggy-Bank(完全背包)
题意:存钱罐自身重量为 ,投入硬币后总重量为 ,现在有 种硬币,数量无限,每种硬币都有自身的价值 与重量 ,问存钱罐里面最少可能有多少钱。
范围:
分析:完全背包变形,从求背包最大价值变成求最小价值,改一下 数组的初始值以及转移方程时的 就可以了。
设 表示可以选择 中的物品并且容量为 时的最小值
转移方程:
当
当
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500 + 10;
const int INF = 0x3f3f3f3f;
int n, w1, w2;
int value[MAXN], weight[MAXN];
int dp[MAXN][10010];
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> w1 >> w2 >> n;
int tot = w2 - w1; // 真正的背包容量
for (int i = 1; i <= n; i++)
{
cin >> value[i] >> weight[i];
}
// 注意初始化
for (int i = 0; i <= n; i++)
{
for (int j = 0; j <= tot; j++)
{
dp[i][j] = INF;
}
}
// 完全背包变形,并且是恰好背包
dp[0][0] = 0;
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= tot; j++)
{
if (j >= weight[i])
{
dp[i][j] = min(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
}
else
{
dp[i][j] = dp[i - 1][j];
}
}
}
if (dp[n][tot] >= INF)
{
cout << "This is impossible." << endl;
}
else
{
cout << "The minimum amount of money in the piggy-bank is " << dp[n][tot] << "." << endl;
}
}
return 0;
}
1018:Free DIY Tour(dp+路径记录)
题意:需要从城市 到 城市 ,每个城市都有自己的兴趣点数,每个城市能单向到达的城市已经给出,问能获得的最大兴趣点数是多少并且输出路径。
范围:
分析:本题的 思路还是清晰的,细节比较多。
考虑每个城市只能从直接到达该城市的城市来,那么我们只需要取这些城市中的最优解就可以了。
设 表示从城市 旅行到城市 所获得的最大兴趣点
转移方程:
城市 能直接到达城市
此外我们用数组 辅助, 表示城市 最优解的上一个城市序号,然后使用栈倒序输出即可。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100 + 10;
int n, m;
int arr[MAXN], dp[MAXN], pre[MAXN];
vector<int> E[MAXN]; // 保存边
stack<int> st; // 用于输出路径
int main()
{
int T;
cin >> T;
int kase = 1;
while (T--)
{
memset(dp, 0, sizeof(dp)); // 清空数组
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> arr[i];
E[i].clear(); // 清空边集
}
arr[n + 1] = 0; // 1和n+1的兴趣点都是0
cin >> m;
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
E[v].push_back(u);
}
// 根据转移方程进行更新dp数组,同时记录前继
for (int i = 1; i <= n + 1; i++)
{
for (auto x : E[i])
{
if (dp[x] + arr[i] > dp[i])
{
dp[i] = dp[x] + arr[i];
pre[i] = x;
}
}
}
cout << "CASE " << kase++ << "#" << endl;
cout << "points : " << dp[n + 1] << endl;
cout << "circuit : ";
// 从后往前将前继一一加入栈
int now = n + 1;
while (now)
{
st.push(now);
now = pre[now];
}
// 利用栈倒叙输出路径
while (!st.empty())
{
if (st.top() == n + 1)
cout << 1;
else
cout << st.top();
st.pop();
if (!st.empty())
cout << "->";
}
cout << endl;
// 输出块之间有空行
if (T)
cout << endl;
}
return 0;
}
1020:最大报销额(01背包+细节)
题意:报销发票。允许报销的发票类型 类、 类、 类,要求每张发票的总额不得超过 元,每张发票中单项物品的价值不得超过 元。现在需要在给出的一堆发票中找出满足报销条件的最大报销额。
范围:
分析:应该可以看出来是 背包吧,但是还有一些细节需要优化与处理。
首先题目中说的单项物品的价值不能超过 元,意思是**同一类**物品的价值不能超过 元
然后 背包的列是容量,是整数,而本题的容量是浮点数,但是注意到只保留小数 位,因此可以全部扩大 倍,变成整数,输出答案的时候再除 。
最后就是解析发票上的每个条目,这里我使用 读入,再使用 来分离类别以及金额。
Notice:一维数组一维数组!二维开不下!
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 30 + 10;
int q, n;
// 最多报销MAXN张1000元的发票,转成int需要再乘100
int value[MAXN], dp[1000 * MAXN * 100];
int main()
{
double t;
while (cin >> t >> n, n)
{
q = t * 100; // q是整数
memset(dp, 0, sizeof(dp)); // 清空数组
int index = 1;
for (int i = 1; i <= n; i++)
{
int m;
cin >> m;
// 分别保存A、B和C类项目的总金额
double sum1 = 0, sum2 = 0, sum3 = 0;
int flag = 1; // 判断该发票是否非法
for (int j = 1; j <= m; j++)
{
string str;
cin >> str;
// 分离类型和金额
stringstream ss(str);
char ch, c;
ss >> ch >> c;
double v;
ss >> v;
// 超过600,非法
if (v > 600)
flag = 0;
// 分类讨论
if (ch == 'A')
sum1 += v;
else if (ch == 'B')
sum2 += v;
else if (ch == 'C')
sum3 += v;
else
flag = 0;
}
// 满足条件才成为背包问题的一个物品
if (flag && sum1 + sum2 + sum3 <= 1000 && sum1 <= 600 && sum2 <= 600 && sum3 <= 600)
{
value[index++] = (sum1 + sum2 + sum3) * 100;
}
}
// 01背包,重量和价值是相同的
for (int i = 1; i < index; i++)
{
for (int j = q; j >= value[i]; j--)
{
dp[j] = max(dp[j], dp[j - value[i]] + value[i]);
}
}
// 注意精度控制,还要除以100
cout << fixed << setprecision(2) << dp[q] * 1.0 / 100 << endl;
}
return 0;
}
三、hard
1019:Regular Words(dp+预处理+高精)
题意:一个字符串只含有 三种字符,定义 为字符串中 的数量, 为字符串中 的数量, 为字符串中 的数量。称一个字符串为合法,当字符串的所有前缀都满足 。现在问长度为 的字符串中有多少满足条件的字符串。
范围:
分析:考虑 ,我们知道变化量是 ,那么就可以用 来代表当前的状态。
当前状态 只能是从 以及 这三种状态转移而来的。
设 表示 时的最大方案数
转移方程:
注意到这道题目的 很小,而且很多组输入,那么我们就可以打表来提高程序的效率。
此外,在 到达 的时候方案数已经非常多了,并且题目没有取模的需求,那么就需要上高精了。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 60 + 10;
const int L = 110; // 高精最多能处理的位数
int n;
string dp[MAXN][MAXN][MAXN]; // 高精表示的dp
// 高精度相加,只限两个非负整数相加
string add(string a, string b)
{
string ans;
int na[L] = {0}, nb[L] = {0};
int la = a.size(), lb = b.size();
for (int i = 0; i < la; i++)
na[la - 1 - i] = a[i] - '0';
for (int i = 0; i < lb; i++)
nb[lb - 1 - i] = b[i] - '0';
int lmax = la > lb ? la : lb;
for (int i = 0; i < lmax; i++)
na[i] += nb[i], na[i + 1] += na[i] / 10, na[i] %= 10;
if (na[lmax])
lmax++;
for (int i = lmax - 1; i >= 0; i--)
ans += na[i] + '0';
return ans;
}
int main()
{
// 将dp数组初始化为0,因为需要上高精,所以是字符串“0”
for (int i = 0; i < MAXN; i++)
{
for (int j = 0; j < MAXN; j++)
{
for (int k = 0; k < MAXN; k++)
{
dp[i][j][k] = "0";
}
}
}
// 需要先对这三种情况进行预处理
dp[1][0][0] = dp[1][1][0] = dp[1][1][1] = "1";
// 根据转移方程进行更新dp数组
for (int i = 2; i < MAXN; i++)
{
for (int j = 0; j <= i; j++)
{
for (int k = 0; k <= j; k++)
{
if (i - 1 >= j)
{
dp[i][j][k] = add(dp[i][j][k], dp[i - 1][j][k]);
}
if (j - 1 >= k)
{
dp[i][j][k] = add(dp[i][j][k], dp[i][j - 1][k]);
}
if (k - 1 >= 0)
{
dp[i][j][k] = add(dp[i][j][k], dp[i][j][k - 1]);
}
}
}
}
// 直接输出答案
while (cin >> n)
{
cout << dp[n][n][n] << endl;
cout << endl;
}
return 0;
}
1021:Largest Rectangle in a Histogram(单调栈)
题意:给出含有 个宽度为 ,高度为 的矩形的直方图,问该直方图中最大的矩形面积是多少?
范围:
分析:首先我们可以知道,最大的矩形是直方图中连续的一段矩形拼接出来的,其高度取决于这一段矩形中的最低高度,那么一个比较直观的方法就是对于每一个宽度为 的矩形,我们把它看做是大矩形中高度最低的矩形,然后看看能往左边有右边分别拓展多长,这样我们就能计算出该大矩形的宽度以及高度,更新答案即可。
那具体怎么往左右两边拓展呢?使用单调栈。
单调栈,栈中的元素时递增的,当读入的数比栈顶的数小时,将栈顶元素弹出直到满足栈中元素递增,在栈顶元素弹出时,说明该弹出的元素能够拓展到的位置最多只能到当前位置。
因此我们可以利用单调栈预处理出第 个矩形往左边拓展的最长距离 以及往右拓展的最长距离 ,之后遍历每个矩形,计算面积,更新答案。
Notice:注意开 long long
Code:
#include <bits/stdc++.h>
#define int long long // 开longlong
using namespace std;
const int MAXN = 1e5 + 10;
int n;
int arr[MAXN], l[MAXN], r[MAXN];
stack<int> s; // 单调栈
signed main()
{
while (cin >> n && n)
{
for (int i = 1; i <= n; i++)
cin >> arr[i];
// 最左边以及最右边设置成最低高度,方便后面统一操作
arr[0] = arr[n + 1] = -1;
// 处理右边拓展长度r[i]数组
for (int i = 1; i <= n + 1; i++)
{
// 如果比栈顶元素小则弹出元素,记录长度
while (!s.empty() && arr[i] < arr[s.top()])
{
r[s.top()] = i;
s.pop();
}
s.push(i); // 加入单调栈
}
// 清空栈
while (!s.empty())
s.pop();
// 处理左边拓展长度l[i]数组
for (int i = n; i >= 0; i--)
{
while (!s.empty() && arr[i] < arr[s.top()])
{
l[s.top()] = i;
s.pop();
}
s.push(i);
}
while (!s.empty())
s.pop();
// 遍历每个矩形计算最大面积统计答案
int ans = 0;
for (int i = 1; i <= n; i++)
{
int R = r[i] - 1, L = l[i] + 1;
ans = max(ans, arr[i] * (R - L + 1));
}
cout << ans << endl;
}
}
1022:City Game(最大1矩阵)
题意:给 的城市分布图,包含 和 ,分别表示未占用与占用,现在要求出未占用矩形的最大面积 。
范围:
分析:经典 问题,最大 矩阵,需要掌握。
预处理高度 数组, 表示点 上面连续 的高度。
有了高度数组,我们可以在 的时间内处理出一行中所有点 左边和右边连续 的长度,同时满足这些点的高度不低于该点的高度。
只要左边的高度不低于当前点的高度,那么就可以继续往左拓展,往右同理,这样就可以计算出当前点可以拓展的长度。
除此以外,我们可以利用面前求出来的 和 数组,如果当前数字 比前一个数字 小,那么一定至少可以拓展到 的位置,然后继续判断是否可以再往左拓展,右边同理,因此算出该行所有点的 和 数组是 的。
程序中虽然是双重循环,但是实际上遍历 和 数组的总次数很少,因此跑得快。
那么我们处理出了高度 数组、左右拓展数组 和 ,那么接下来我们只需要双重循环遍历每个点,左右拓展之后的宽度*该点的高度=该点所在当前矩形的面积,更新答案即可。
Notice:本题推荐使用 ,或者像我使用 关闭流同步也可以。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000 + 10;
int n, m;
int g[MAXN][MAXN], h[MAXN][MAXN], l[MAXN], r[MAXN];
int main()
{
// 关闭流同步
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin >> T;
while (T--)
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
char ch;
cin >> ch;
// 转换成01矩阵
g[i][j] = ch == 'R' ? 0 : 1;
// 处理高度数组
if (g[i][j])
h[i][j] = h[i - 1][j] + 1;
else
h[i][j] = 0;
}
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
// 对于每行初始化l和r数组为自身
for (int j = 1; j <= m; j++)
{
l[j] = r[j] = j;
}
// 设置两边监视哨,方便后序操作统一
h[i][0] = h[i][m + 1] = -1;
// 处理该行所有点的l数组
for (int j = 1; j <= m; j++)
{
while (h[i][j] <= h[i][l[j] - 1])
{
l[j] = l[l[j] - 1];
}
}
// 处理该行所有点的r数组
for (int j = 1; j <= m; j++)
{
while (h[i][j] <= h[i][r[j] + 1])
{
r[j] = r[r[j] + 1];
}
}
// 更新答案
for (int j = 1; j <= m; j++)
{
ans = max(ans, (r[j] - l[j] + 1) * h[i][j]);
}
}
cout << ans * 3 << endl;
}
return 0;
}
【END】感谢观看!