题目链接:https://www.luogu.org/problemnew/show/P1541
一道难度适中的DP,特别需要注意的地方是状态的构建。
一开始看到这道题目,先想到的状态是dp[N][M][M][M][M],其中N表示到了哪一步,M表示4种牌当前我们手中分别持有的数量,但这样显然是炸了空间,所以就想利用总牌数减去前三种牌的数量来优化掉第四维,但由于水平有限,没有搞出来。这时又看到了牌数和步数的关系,也就是与我们当前所处位置的关系,依据这个就不难把N那一维优化掉,具体看代码吧,转移还是很好想的。
code:
//记忆化写法
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 351, M = 41;
int n, m, has1, has2, has3, has4, a[N], dp[M][M][M][M];
int get_p(int h1, int h2, int h3, int h4) {
return n-(h1+h2*2+h3*3+h4*4);
}
int dfs(int h1, int h2, int h3, int h4) {
int p = get_p(h1, h2, h3, h4);
if (p==1) return dp[has1][has2][has3][has4];
if (dp[h1][h2][h3][h4]) return dp[h1][h2][h3][h4];
if (h1+1<=has1) dp[h1][h2][h3][h4] = max(dfs(h1+1, h2, h3, h4)+a[p], dp[h1][h2][h3][h4]);
if (h2+1<=has2) dp[h1][h2][h3][h4] = max(dfs(h1, h2+1, h3, h4)+a[p], dp[h1][h2][h3][h4]);
if (h3+1<=has3) dp[h1][h2][h3][h4] = max(dfs(h1, h2, h3+1, h4)+a[p], dp[h1][h2][h3][h4]);
if (h4+1<=has4) dp[h1][h2][h3][h4] = max(dfs(h1, h2, h3, h4+1)+a[p], dp[h1][h2][h3][h4]);
return dp[h1][h2][h3][h4];
}
int main() {
int i, j, k, x;
cin >> n >> m;
for (i = 1 ; i <= n; i++)
cin >> a[i];
for (i = 1; i <= m; i++) {
cin >> x;
if (x==1) has1++;
if (x==2) has2++;
if (x==3) has3++;
if (x==4) has4++;
}
dp[has1][has2][has3][has4] = a[1];
cout << dfs(0, 0, 0, 0);
return 0;
}
//循环写法
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 351, M = 41;
int n, m, use1, use2, use3, use4, a[N], dp[M][M][M][M];
int get_p(int u1, int u2, int u3, int u4) {
return n-(u1+u2*2+u3*3+u4*4);
}
/*int dfs(int u1, int u2, int u3, int u4) {
int p = get_p(u1, u2, u3, u4);
if (p==1) return dp[use1][use2][use3][use4];
if (dp[u1][u2][u3][u4]) return dp[u1][u2][u3][u4];
if (u1+1<=use1) dp[u1][u2][u3][u4] = max(dfs(u1+1, u2, u3, u4)+a[p], dp[u1][u2][u3][u4]);
if (u2+1<=use2) dp[u1][u2][u3][u4] = max(dfs(u1, u2+1, u3, u4)+a[p], dp[u1][u2][u3][u4]);
if (u3+1<=use3) dp[u1][u2][u3][u4] = max(dfs(u1, u2, u3+1, u4)+a[p], dp[u1][u2][u3][u4]);
if (u4+1<=use4) dp[u1][u2][u3][u4] = max(dfs(u1, u2, u3, u4+1)+a[p], dp[u1][u2][u3][u4]);
return dp[u1][u2][u3][u4];
}*/
int main() {
int i, j, k, l, x;
cin >> n >> m;
for (i = 1 ; i <= n; i++)
cin >> a[i];
for (i = 1; i <= m; i++) {
cin >> x;
if (x==1) use1++;
if (x==2) use2++;
if (x==3) use3++;
if (x==4) use4++;
}
dp[use1][use2][use3][use4] = a[1];
for (i = use1; i >= 0; i--) {
for (j = use2; j >= 0; j--) {
for (k = use3; k >= 0; k--) {
for (l = use4; l >= 0; l--) {
int p = get_p(i, j, k, l);
if (i == use1 && j == use2)
if (k == use3 && l == use4) continue;
if (i+1<=use1) dp[i][j][k][l] = max(dp[i+1][j][k][l]+a[p], dp[i][j][k][l]);
if (j+1<=use2) dp[i][j][k][l] = max(dp[i][j+1][k][l]+a[p], dp[i][j][k][l]);
if (k+1<=use3) dp[i][j][k][l] = max(dp[i][j][k+1][l]+a[p], dp[i][j][k][l]);
if (l+1<=use4) dp[i][j][k][l] = max(dp[i][j][k][l+1]+a[p], dp[i][j][k][l]);
}
}
}
}
cout << dp[0][0][0][0];
return 0;
}
注意题目中说的分数为非负数,因此也可能出现第一个点的分数为0的情况,可能会影响记忆化搜索,但好在题目数据比较弱,所以我的代码中是没有考虑这种情况的(懒得改嘛)。
如果这种记忆化(因为我个人的习惯,喜欢用类似于爆搜那样的方式来写)看着不舒服的话也可以换别的方式来写。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
题目链接:https://www.luogu.org/problemnew/show/P1140#sub
个人认为比乌龟棋还要难的dp,状态和转移都比较难想,状态是dp[i][j],表示第一个基因到了i碱基,第二个基因到了第j个碱基,这时候的最大相似度,注意i和j只是指原基因的下标,如果有新插入的碱基,i和j不需要移动。
转移的话也比较好想,就是要注意不要乱了思路,时刻保持思路清晰。另外建议在转移前将基因配对的表给打好。
还有一个比较坑的地方就是相似度可以为负值,因此一开始数组的初始化要特别注意。
code:
//循环写法
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<string>
using namespace std;
const int N = 1e2+10, MIN_INF = -1e9;
int n, m, a[N], b[N], dp[N][N];
int tab[5][5] = {{5,-1,-2,-1,-3},
{-1,5,-3,-2,-4},
{-2,-3,5,-2,-2},
{-1,-2,-2,5,-1},
{-3,-4,-2,-1,0}};
int main() {
int i, j;
string s1, s2;
cin >> n >> s1;
for (i = 0; i < n; i++) {
if (s1[i] == 'C') a[i+1] = 1;
if (s1[i] == 'G') a[i+1] = 2;
if (s1[i] == 'T') a[i+1] = 3;
}
cin >> m >> s2;
for (i = 0; i < m; i++) {
if (s2[i] == 'C') b[i+1] = 1;
if (s2[i] == 'G') b[i+1] = 2;
if (s2[i] == 'T') b[i+1] = 3;
}
for (i = 1; i <= N; i++)
for (j = 1; j <= N; j++)
dp[i][j] = MIN_INF;
dp[0][0] = 0;
for (i = 1; i <= n; i++) dp[i][0] = dp[i-1][0]+tab[a[i]][4];
for (i = 1; i <= m; i++) dp[0][i] = dp[0][i-1]+tab[4][b[i]];
for (i = 1; i <= n; i++) {
for (j = 1; j <= m; j++) {
dp[i][j] = max(dp[i][j], dp[i-1][j-1]+tab[a[i]][b[j]]);
dp[i][j] = max(dp[i][j], dp[i-1][j]+tab[a[i]][4]);
dp[i][j] = max(dp[i][j], dp[i][j-1]+tab[4][b[j]]);
}
}
cout << dp[n][m];
return 0;
}
//记忆化基因
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<string>
using namespace std;
const int N = 1e2+10, MIN_INF = -1e9;
int n, m, a[N], b[N], dp[N][N];
int tab[5][5] = {{5,-1,-2,-1,-3},
{-1,5,-3,-2,-4},
{-2,-3,5,-2,-2},
{-1,-2,-2,5,-1},
{-3,-4,-2,-1,0}};
bool vis[N][N];
int dfs(int i, int j) {
if (vis[i][j]) return dp[i][j];
vis[i][j] = true;
dp[i][j] = max(dp[i][j], dfs(i-1, j-1)+tab[a[i]][b[j]]);
dp[i][j] = max(dp[i][j], dfs(i-1, j)+tab[a[i]][4]);
dp[i][j] = max(dp[i][j], dfs(i, j-1)+tab[4][b[j]]);
return dp[i][j];
}
int main() {
int i, j;
string s1, s2;
cin >> n >> s1;
for (i = 0; i < n; i++) {
if (s1[i] == 'C') a[i+1] = 1;
if (s1[i] == 'G') a[i+1] = 2;
if (s1[i] == 'T') a[i+1] = 3;
}
cin >> m >> s2;
for (i = 0; i < m; i++) {
if (s2[i] == 'C') b[i+1] = 1;
if (s2[i] == 'G') b[i+1] = 2;
if (s2[i] == 'T') b[i+1] = 3;
}
for (i = 1; i <= n; i++)
for (j = 1; j <= m; j++)
dp[i][j] = MIN_INF;
dp[0][0] = 0, vis[0][0] = true;
for (i = 1; i <= n; i++) {
dp[i][0] = dp[i-1][0]+tab[a[i]][4];
vis[i][0] = true;
}
for (i = 1; i <= m; i++) {
dp[0][i] = dp[0][i-1]+tab[4][b[i]];
vis[0][i] = true;
}
cout << dfs(n, m);
return 0;
}
-------------------------------------------------------------------------------------------------------------------------------------------------------
题目链接:https://www.luogu.org/problemnew/show/P1435
首先这道题一个很显然的性质,对于一个偶数长度的回文串,如果我们从中间插入一个字符,那么它还是回文串;奇数长度的我们在中间插入一个与原中间字符相同的字符也可以保持其回文的性质。
那么基于这个,我们就可以设计状态f[i][j],表示前i个和后j个组成回文串所要插入的最少字符,这样我们每次就可以在中间插入字符,没有后效性。(表示一开始没想到这个状态,调了一上午QWQ)
在熟悉状态之后,边界和转移也就不难写出了,具体可以看代码。
还有需要注意的就是在最后枚举答案时不要忘了奇数长度的情况,即中间字符不要算在回文串内,还有数组不要越界。
code:
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
const int N = 1e3+10, INF = 0x3f3f3f3f;
string s;
int ans = INF, f[N][N];
int main() {
memset(f, INF, sizeof(f));
int i, j, len;
cin >> s;
len = s.size();
for (i = 0; i <= len; i++) {
f[0][i] = i;
f[i][0] = i;
}
for (i = 1; i <= len; i++) {
for (j = 1; j+i <= len; j++) {
int p1 = i-1, p2 = len-j;
if (s[p1]==s[p2]) f[i][j] = f[i-1][j-1];
else {
f[i][j] = min(f[i][j], f[i-1][j]+1);
f[i][j] = min(f[i][j], f[i][j-1]+1);
}
}
}
for (i = 0; i < len; i++)
ans = min(min(f[i][len-i], ans), f[i][len-i-1]);
cout << ans;
return 0;
}
感觉这道题有些难度,主要是状态比较难想,还要注意好一些细节。
------------------------------------------------------------------------------------------------------------------------------------------------------------
题目链接:https://www.luogu.org/problemnew/show/P1970
其实是蛮简单的一道DP,然而我还是写了一上午,主要是状态搞错了(所以说DP状态真的很重要,状态对了,转移自然就出来了,而且写起来还简单)。f[i][0]表示到i上升的最长波浪序列,f[i][1]表示到i最长下降的波浪序列,转移见代码。(注意这个转移一定要把当前每个状态都处理好,这也是DP最容易犯错的地方,甚至可能爆0,只要每个状态都处理好,后面递推的才正确嘛)
code:
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1e6+10;
int n, a[N], f[N][2];
int main() {
int i;
cin >> n;
for (i = 1; i <= n; i++)
cin >> a[i];
f[1][0] = f[1][1] = 1;
for (i = 2; i <= n; i++) {
if (a[i]>a[i-1]) f[i][0] = f[i-1][1]+1;
else f[i][0] = f[i-1][0];
if (a[i]<a[i-1]) f[i][1] = f[i-1][0]+1;
else f[i][1] = f[i-1][1];
}
cout << max(f[n][0], f[n][1]);
return 0;
}
-------------------------------------------------------------------------------------------------------------------------------------------------------
https://www.luogu.org/blog/user16215/ju-zhen-qu-shuo-lei-dp矩阵取数类DP
------------------------------------------------------------------------------------------------------------------------------------------------------
总结:这三道dp都是属于随便dp的那种,也是所有dp中最重要的一种dp,因为是其它dp的基础嘛。
dp的题目可以想成是在一张DAG上跑最短路和最长路,说白了,所有的dp都是图论,只要能够建起图(包括找到状态确定点,和转移连边),就可以轻松解决了。
dp找对状态真的很重要!!