目录
动态规划:
板块一:一维简单dp(简单,但不一定能够想到,都是泪)
第一题:删数游戏
第二题:取物问题
第三题:填坑问题
第四题:跳跳跳(LIS最长递增子序列的系列问题)
第五题:钱币兑换问题(完全背包)
第六题:完全背包 + 填满背包 + 最小价值
第七题:网格覆盖问题(无法完全填满的)
板块二:二维动态规划
第一题:求包含某子串的排列数
第二题:数塔问题
板块三:状态压缩动态规划
第一题:吃奶酪(状态压缩dp基础题,求经过所有点的最小路径)
贪心:
板块一:简单贪心
第一题:排队接水
第二题:线段覆盖
第三题:反复取最小的两数
第四题:删数问题
第五题:分组,求连续序列的最小值的最大值
第六题:搬桌子,求最多的重叠的次数
第七题:填坑问题(对区间操作,求最小操作数)
动态规划和贪心感觉有些类似,无非都是求一个最优的状态,利用历史数据。
批注:这道题的意思是,给你一个数列,你可以删去其中的一个数,并得到这个数的分数,但是这是有代价的,你会失去这个数+1-1的两个数,求最大的分数值。
状态转移方程:
含义:dp是用来记录从0到n的最大分数的数组,num是用来记录某个数n的数目的数组。
显然,dp[0] = 0,没有数可以删,自然分数为0.
显然,dp[n]是一个单调不降的数列,可以选择空间越大自然越容易得分。如果说其中某些数n的个数为0,就会直接继承所有前面的数,也可以认为个数为0是一种合法的个数,只不过个数为0罢了。
分析:
如果,如果因为种种原因,n不存在(已经被删,个数为0,主动不删)没有删除n,那么就继承最大的dp[n-1](dp[n-2]一定不大于dp[n-1]).
如果,n存在,选并且择删掉n。下面是关键:为什么是dp[n-2],我们要保证n没有被删,那么n-1肯定不能主动删,因此我们选择dp[n-2]因为,即使删掉了n-2最多导致,n-1被删,不会导致n被删,而且dp[n-2]的状态是能够保证n存在的最大的值。
注意:long long,注意数组越界。
代码:
#include
const int N = 1e5 + 5;
using namespace std;
long long a[N] = {0}, dp[N] = {0};
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
int m = 0;
for (int i = 0; i < n; i++) {
int b;
cin >> b;
a[b]++;
m = max(m, b);
}
dp[1] = a[1];
for (int i = 2; i <= m; i++) {
dp[i] = max(dp[i - 1], dp[i - 2] + a[i] * i);
}
cout << dp[m] << '\n';
return 0;
}
这道题也是线性递推,注意分类思想,我们有两种情况,第一种情况是第i个数已经被取了,那么我们要选第一个可以取的,而且,在所有导致i被取的dp值里面最大的就是上一个,如果说第i的数没有被取,那么说明k个数没有被取,取k+1之前的数,状态转移方程如下:
注意非负。
#include
using namespace std;
const int N = 2005;
int dp[N], a[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
dp[0] = 0;
dp[1] = a[1];
for (int i = 2; i <= n; i++) {
dp[i] = max(dp[max(0, i - k - 1)] + a[i], dp[i - 1]);
}
cout << dp[n] << '\n';
return 0;
}
这是简单的线性DP,或者说贪心,需要思考的是。
dp含义:表示填满前i个坑所需要的最小操作次数。
最小的子问题:如果说只有一个坑的话,为了填满它我们必须填坑的次数就算坑的深度。
状态转移:把前面所以坑填满的最小次数,和这个坑之间的关系,如果说这个坑比上面一个坑浅的话可以在填上一个坑的时候顺带把这个坑填了,次数不变,如果这个坑比上面的坑要深的话,除了顺带填到等深处还要另外填多出来的部分。
状态转移方程:
if (a[i] <= a[i - 1]) {
dp[i] = dp[i - 1];
} else {
dp[i] = dp[i - 1] + a[i] - a[i - 1];
}
完整代码:
#include
using namespace std;
const int N = 1e5 + 5;
int dp[N], a[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
dp[1] = a[1];
for (int i = 2; i <= n; i++) {
if (a[i] <= a[i - 1]) {
dp[i] = dp[i - 1];
} else {
dp[i] = dp[i - 1] + a[i] - a[i - 1];
}
}
cout << dp[n] << '\n';
return 0;
}
LIS是动态规划的经典问题,最佳方法是nlogn的方法,树状数组维护之类的,这里数据比较小用的是n方的基础思想。dp数组的含义很重要,这里dp的含义是到达某点时的最大分值,必然先前出现的某一个值的最大值加上这个值,只要把状态转移方程中的a[i]改成1就是求最长递增子序列的状态转移方程,下面是代码:
#include
using namespace std;
const int N = 1005;
int a[N], dp[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
while (cin >> n, n) {
memset(dp, 0, sizeof(dp));
a[0] = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
if (a[i] > a[j]) {
dp[i] = max(dp[i], dp[j] + a[i]);
}
}
}
cout << *max_element(dp, dp + n + 1) << '\n';
}
return 0;
}
自己的总结链接
这道题似乎也是可以用母函数去做但是,会超时:(,母函数确实是万能的,不过如果不会FFT的阉割版本很容易超时,而且,有时候杀鸡用牛刀了,剪枝后的母函数约为O(N^2),而一维的完全背包的时间复杂度则是O(n)。
可以看看代码:
#include
#define int long long
using namespace std;
const int N = 32770;
int c1[N], c2[N];
main() {
ios::sync_with_stdio(false);
cin.tie(0);
int v[5] = {1, 2, 3};
int n;
while (cin >> n) {
for (int i = 0; i <= n; i++) {
c1[i] = 0;
c2[i] = 0;
}
c1[0] = 1;
for (int i = 0; i < 3; i++) {
for (int j = 0; j * v[i] <= n; j++) {
for (int k = 0; k + j * v[i] <= n; k++) {
c2[k + v[i] * j] += c1[k];
}
}
for (int j = 0; j <= n; j++) {
c1[j] = c2[j];
c2[j] = 0;
}
}
cout << c1[n] << '\n';
}
return 0;
}
正解:
#include
using namespace std;
int dp[40000], a[4] = {0, 1, 2, 3};
int main() {
int n;
while (cin >> n) {
memset(dp, 0, sizeof(dp));
dp[0] = 1;
for (int i = 1; i <= 3; i++) {
for (int j = a[i]; j <= n; j++) {
dp[j] = dp[j] + dp[j - a[i]];
}
}
cout << dp[n] << '\n';
}
return 0;
}
为此我们可以总结一下:
母函数的结果是多项式,反映了各个时刻不同重量(价值)的方案数。
而完全背包,给出的到某个值的方案数。
背包问题其实是母函数的一种特解,时间复杂度较低,但是并不是所有的问题都能用背包来解决,尤其是在求方案数的时候,注意,方案数和最大价值不同。
背包问题常常用来求最大价值,但是这里却可以用来求最小值,因为要求填满,我们只需要根据题意进行松弛操作,就可以写出状态转移方程了,这里用min而且,初始化的时候dp[0] = 0;其他的为正无穷,如果是一般的填满背包是负无穷。
实现:
#include
using namespace std;
const int N =3001000, M = 505;
int v[M], w[M], dp[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int t;
cin >> t;
while (t--) {
int a, b;
cin >> a >> b;
int m = b - a;
memset(dp, 0x3f, sizeof(dp));
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> w[i] >> v[i];
}
dp[0] = 0;
for (int i = 0; i < n; i++) {
for (int j = v[i]; j <= m; j++) {
dp[j] = min(dp[j], dp[j - v[i]] + w[i]);
}
}
if (dp[m] == 0x3f3f3f3f) {
cout << "This is impossible.\n";
} else {
cout << "The minimum amount of money in the piggy-bank is " << dp[m] << ".\n";
}
}
return 0;
}
推导
这里要注意完成最后部分填充的是各不相同的,无法简单地被包含!!!。
#include
#define ll long long
using namespace std;
ll num[40];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
num[0] = 1;
num[1] = 0;
num[2] = 3;
for (int i = 3; i <= 35; i++) {
if (i % 2) num[i] = 0;
else num[i] = num[i - 2] * 4 - num[i - 4];
}
while (cin >> n) {
if (n == -1) break;
cout << num[n] << '\n';
}
return 0;
}
这到题要我们求含有NBT子串的排列数,如果用一维递推我们会发现这很棘手,假设我们定义一维dp数组的含义是含有NBT子串的排列数,那么对于dp[n]的来源有两种,本来有NBT然后随便加上一个字母,也就是dp[n - 1] * 26但是,还有一种可能,是本来没有的,加上一个T就i出现了NBT,这就麻烦了,我们只知道含有NBT的排列数,不知道只含有NB的子串,但是问题似乎是变得简单了一些,如果我们知道NB的排列数,那么是不是只需要知道只含有N的字串,再进一步如果是不含N的字串呢?发现问题就解决了,不含N的字串我们很容易就可以知道,就是25^n。所谓动态规划就是要利用历史数据构造出新的数据,而且新的数据和旧的数据具有相同的性质。
dp的含义:
第一维表示字符串的长度,第二维表示串的状态。
第二维是0表示不含N的串
第二维是1表表示含N但不含NB的串
第二维是2表示含NB但不含NBT的串
第二维是3表示含NBT的串
这是初始边界:
dp[1][0] = 25;
dp[1][1] = 1;
dp[1][2] = 0;
dp[1][3] = 0;//长度不够
状态转移方程:
dp[i][0] = dp[i - 1][0] * 25;//如果没有N只需加非N字符
dp[i][1] = dp[i - 1][1] * 25 + dp[i - 1][0];//两种来源,只含N加非B,不含N加N,
dp[i][2] = dp[i - 1][2] * 25 + dp[i - 1][1];//两种来源,只含NB加非T,只含N加B
dp[i][3] = 26 * dp[i - 1][3] + dp[i - 1][2];//两种来源,含NBT加任意,只含NB加T
代码:
#include
#define ll long long
using namespace std;
const int N = 2e5 + 5;
const ll p = 1e9 + 7;
ll dp[N][4];
void get_dp() {
dp[1][0] = 25;
dp[1][1] = 1;
dp[1][2] = 0;
dp[1][3] = 0;
for (ll i = 2; i <= 2e5; i++) {
dp[i][0] = dp[i - 1][0] * 25;
dp[i][0] %= p;
dp[i][1] = dp[i - 1][1] * 25 + dp[i - 1][0];
dp[i][1] %= p;
dp[i][2] = dp[i - 1][2] * 25 + dp[i - 1][1];
dp[i][2] %= p;
dp[i][3] = 26 * dp[i - 1][3] + dp[i - 1][2];
dp[i][3] %= p;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int t;
cin >> t;
get_dp();
while (t--) {
int n;
cin >> n;
cout << dp[n][3] << '\n';
}
return 0;
}
数塔问题是非常经典的二维dp问题,建议逆推,路径是双向的,直接把二维数组当作容器安放数塔,倒推的时候状态如何转移,起点的dp值是本身,其他点的值为本身加上可能的前驱的最大值,dp的含义就是以当前为塔顶的最大和,注意边界,注意零,答案输出为塔顶坐标,这里为什么不需要考虑无法到达起点的点,因为这些点不会被计算到最终的答案中,所以不必分类讨论,这里坐标加1也是为了少考虑边界条件,把核心关系写清楚。
#include
using namespace std;
const int N = 1e5 + 5;
int dp[15][N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
while (cin >> n, n) {
memset(dp, 0, sizeof(dp));
int maxn = 0;
while (n--) {
int p, t;
cin >> p >> t;
dp[p + 1][t]++;
maxn = max(maxn, t);
}
for (int i = maxn; i >= 0; i--) {
for (int j = 1; j <= 11; j++) {
dp[j][i] = max(dp[j][i + 1], max(dp[j - 1][i + 1], dp[j + 1][i + 1])) + dp[j][i];
}
}
cout << dp[6][0] << '\n';
}
return 0;
}
状态压缩动态规划的核心主要是通过位来反映状态的变化,从而节省空间和时间,下面作了详细的分析。构造二进制串来反映状态,利用状态转移方程来解决问题,如果用上bitset还可以更进一步化简。
#include
using namespace std;
double a[20][20], x[20], y[20], dp[18][34000];//当前在i点,状态为j(10进制)=(xxx01xx10xxxxx)(2进制),经过1的点,的路径的下的最小值
int N;
double dis(int v, int w) {
return sqrt(((x[v] - x[w]) * (x[v] - x[w]) + (y[v] - y[w]) * (y[v] - y[w])));
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);//这个比较玄学,注意输出不要混用
memset(dp, 0x7f, sizeof(dp));//注意给浮点数赋值最大需要0x7f
double ans = dp[0][0];
cin >> N;
for (int i = 1; i <= N; i++) {
cin >> x[i] >> y[i];
}
x[0] = 0, y[0] = 0;
for (int i = 0; i <= N; i++) {
for (int j = i + 1; j <= N; j++) {
a[i][j] = dis(i, j);//矩阵存储距离
a[j][i] = a[i][j];
}
}
for (int i = 1; i <= N; i++) {
dp[i][1 << (i - 1)] = a[0][i];//观察位移的情况可以知晓,这是刚好过第i个点(从右往左数)的距离(从原点出发),比如1为dp[1][1]
}//如果2则是dp[2][(10)(二进制)]
for (int k = 1; k < (1 << N); k++) {//对于每一个k,当前可能在任何一个1上
for (int i = 1; i <= N; i++) {//对于在每一个位置上,上一个状态必然是来自其他的1上到第i位的1距离
if ((k & (1 << (i - 1))) == 0) continue;//如果没有到过肯定不予考虑,没有在路径当中
for (int j = 1; j <= N; j++) {
if (i == j) continue;//必须是其他的点才可以转移过来,有前后关系
if ((k & (1 << (j - 1))) == 0) continue;
dp[i][k] = min(dp[i][k], dp[j][k - (1 << (i - 1))] + a[i][j]);//状态转移方程从随着k的增大逐渐转移,从较小的问题不断累积,从而解决最后的问题
}//状态转移方程,dp[i][(10011001010101000)(k)]必然来自,某个非第i位的1+a[i][j],转移而来
}
}
for (int i = 1; i <= N; i++) ans = min(ans, dp[i][(1 << N) - 1]);//指的是经过了所有的点的状态的的最小值
printf("%.2lf\n", ans);//这里仍然能够使用
return 0;
}
这题题目感觉不是很清楚,在接水的不算等待时间。这里其实用了一个排序不等式,也就是:
反序和 <= 乱序和 <= 正序和。网上资料可以看看:知乎
代码:
#include
using namespace std;
const int N = 1005;
int a[N], b[N];
bool cmp(int x, int y) {
return a[x] < a[y];//间接排序可以避免用结构体
}
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
b[i] = i;
}
sort(b, b + n, cmp);//小的在前面,给全体带来的时间积累最小
long long sum = 0;
for(int i = 0; i < n; i++) {
cout << b[i] + 1;//序号要加一
if (i != n - 1) {
cout << ' ';
} else {
cout << '\n';
}
sum += a[b[i]] * (n - i - 1);//小坑点
}
printf("%.2lf\n", sum * 1.0 / n );
return 0;
}
因为时间是有上界的,所以只要知道结束的时间,就可以判断某场比赛在一个闭区间上[0,ED]。
直观地看为什么,终止时间越早越好,因为这样可以给后来的比赛更多的时间空间,更有可能比赛。稍微严谨地看,对于区间[0, ED]有三种情况,第一个没有任何与这个区间相交的情况,直接白嫖加1,第二种,包含在其中的其他区间,这些都是等价的,因为经过排序,不可能有在其中结束的其他区间,第三种,有后面结束的区间相交,那么如果我们取后面的区间,总的数目是不变的,但是耗费的区间更多了,并没有带来优化,动态规划的思维,一直递推下去我们就发现这样考虑是最优的,正因为第三种情况的存在,所以区间的摆放数最多时候的情况不是唯一的。
大功告成。
下面是代码:
#include
using namespace std;
const int N = 1e6 + 5;
struct unit {
int st, ed;
} a[N];
bool cmp (unit x, unit y) {
return x.ed < y.ed;
}
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i].st >> a[i].ed;
}
sort(a, a + n, cmp);
int cnt = 0,now = 0;//now记录上一个结束的位置,相当于新的区间起点
for (int i = 0; i < n; i++) {
if (a[i].st >= now) {
now = a[i].ed;
cnt++;
}
}
cout << cnt << '\n';
return 0;
}
用优先队列即可,优先队列默认的是大顶堆,要用小顶堆的话要自己修改,注意这里不能写非空因为一个的时候不用再做了,而且这样写会RE。
下面是代码:
#include
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
priority_queue, greater > q;
int n;
cin >> n;
while (n--) {
int c;
cin >> c;
q.push(c);
}
long long sum = 0;
while (q.size() >= 2) {
int x1 = q.top();
q.pop();
int x2 = q.top();
q.pop();
sum += x1 + x2;
q.push(x1 + x2);
}
cout << sum << '\n';
return 0;
}
对于任意的一个数123423554647567457346535如何删除一个数使得数变得最小。结论,删掉最长不降序列的最后一个。
做差比较一下:
******************************* ****************************** ******** *
******************************* ****************************** 区别是 ******** *
如果是相邻的话
那么就一定是一定是删除后面的比较好,因为上面删除的是最后一个数,那么黑色的数一定是小于的,如果不是的话,那么黑色的星一定是更大的数。如果在序列里面的话,删后面的会比较前面的数,更小。那么,会不会有可能在后面的序列中删除一个数会更小呢?
也就是比较
********
********
不会,因为,这两个数第一位就比出来了!。如果是删除最后一位首位是更小的数,如果是后面的某个数的话,首位也就是黑*,必然是最大的数,肯定不存在更小的数。
注意两个点,第一个前导零的去除,第二个是要注意数字重复的时候不要着急删数,没准后序有更大的因为要求的时候是最长不降序列。
#include //网络流
using namespace std;
char a[255];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int b;
cin >> a >> b;
int len = strlen(a);
while (b--) {
while (a[0] == '0') {//可能有一堆前导零
for (int i = 0; i < len; i++) {
a[i] = a[i + 1];//删数用覆盖的方法
}
len--;
}
for (int i = 0; i < len; i++) {
if (a[i + 1] < a[i] || a[i + 1] == '\0') {//递减和末尾都要删
for (int j = i; j < len; j++) {
a[j] = a[j + 1];
}
len--;//长度减一模拟删数
break;
}
}
}
for (int i = 0; i < len; i++) {
cout << a[i];
}
cout << '\n';
return 0;
}
批注:先排序,从左到右遍历,每次我们都只能更新一个长度,而且这个长度是最小的变化单位也就是1,根据木桶原理,我们每次都要优先补齐人数最小的组,而且要连续可接,为此得到如下代码,其实完全不必使用二分答案。
#include
using namespace std;
const int N = 1e5 + 5;
int n, a[N], b[N], f[N];//a是数据数组,f[i]是第i组的成员数目,b[i]是第i组末尾的实力,实力最强的人
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
sort(a + 1, a + n + 1);//从小到大排序,排序保证加1
int position = 0;//初始组数为0
for (int i = 1; i <= n; i++) {//对于每个元素,考虑放到哪个组里
int point = 0, minx = INT_MAX;
for (int j = 1; j <= position; j++) {//是当前的组数
if (f[j] < minx && b[j] + 1 == a[i]) {//成员数最小而且可以接上去
point = j;//找到成员数最少的可以接上去的组,优先接小的
minx = f[j];//找到最小值的而且是可以接上去的组,接上去
}
}
if (point == 0) {//如果没有找到的话,
f[++position] = 1;//单人组
b[position] = a[i];//记录序列末尾
} else {
b[point] = a[i];//更新序列末尾
f[point]++;//更新队员
}
}
cout << *min_element(f + 1, f + 1 + position) << '\n';
}
由于搬桌子的时间是相同的,所以只需考虑次数便好,非常简单。注意swap。
#include
using namespace std;
int book[205];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int t;
cin >> t;
while (t--) {
memset(book, 0, sizeof(book));
int n;
cin >> n;
while (n--) {
int a, b;
cin >> a >> b;
if (a > b) {
swap(a, b);
}
for (int i = (a + 1) / 2; i <= (b + 1) / 2; i++) {
book[i]++;
}
}
cout << *max_element(book, book + 200) * 10 << '\n';
}
return 0;
}
这道题其实和洛谷的一道题积木大赛是一样的,自取。
注意z的出现可能导致意外的出现,所以我们要通过转动字符串来避免讨论,可能我的思路比较清奇,但是确实有效,注意边界的坑要填上!!。最边界的位置要设置成0。
实现:
#include
using namespace std;
const int N = 1e5 + 5;
int a[N], b[N];//a是转换成标号的数组
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
string s;
cin >> s;
int ans = INT_MAX, len = s.size();
for (int i = 0; i < len; i++) {
a[i + 1] = s[i] - 'a' + 1;
}
b[0] = 0;
for (int i = 0; i < 26; i++) {
int sum = 0, top = 0;
for (int j = 1; j <= len; j++) {
if (a[j] + i > 26) b[j] = (a[j] + i) % 26;//轮换26次,确保最优解被取到
else b[j] = a[j] + i;//这里要讨论,不能简单模26
top = max(top, b[j]);//我们取最高处为基准
}
for (int j = 1; j <= len; j++) {
b[j] = top - b[j];//转换成填坑问题
// cout << b[j];//测试语句,请忽略
}
for (int j = 1; j <= len; j++) {
if (b[j] > b[j - 1]) sum += b[j] - b[j - 1];//核心
}
ans = min(sum, ans);//多次取最优解
}
cout << ans << '\n';
return 0;
}