身为大学开学才开始学习编程的蒟蒻,目前连复杂度都不懂得怎么去分析。一路不断地看题解,"做题目"的数量是多起来了,但是可以明显地感觉到这样做对我没什么卵用。 因此我决定慢慢来,一步一个脚印,就算天天“抄代码"那也得"抄"得认真点!
在学习过程中写博客主要是为了记录自己的思想而不是特意让人看的。
最近DP对我来说比较重要,第一篇学习笔记从DP开始吧。以下内容主要是目前自己认同的思想,有待改善。
动态规划:
动态规划是一种思想和手段,并非一种特定的算法。但是目前我看过的几个题目在运用动态规划上大都是有一定流程的。
1.首先以抽象的思维将问题拆分成子问题,要求子问题与原问题具有相同的解法,并且子问题没有重复性,求解完子问题即可得到原问题的答案。
2.不断分解子问题能找到问题的边界,我认为这个和数学归纳法有些相似,边界就是你可以轻松证明出来的状态,动态规划最重要的是递推。
3.找到DP方程需要的各种变量,初步得出状态转移方程。
4.思考空间和时间是否能优化,是否能将子问题合并或者化简,进而得出最后的状态转移方程。
一定要注意动态规划原则:最优子结构和无后效性
先放个热身题:Ignatius and the Princess IV HDU - 1029
题意:给N个数(N为奇数),其中一个数至少出现(N+1)/2次,求出这个数。
如果是我就直接STL了,但是看过一篇博客感觉好神奇,思路是将不同数成对消除,下面是根据原代码经过改进的正解。这个题目说明了动态规划没有说明固定的流程或套路,主要还是思想。神奇的代码:
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 1000000 + 5;
int N, a[maxn], dp[maxn];
int main(void)
{
while (~scanf("%d", &N))
{
memset(dp, 0, sizeof(dp));
for (int i = 0; i < N; i++)
scanf("%d", &a[i]);
int i = 0, j = 1;
while (j < N)
{
while (dp[i])i++;//假设第一个数为所求数,则j要经过的路径上最少有(N+1)/2-1个所求数,则一定能将非所求数消除
while (j <= N && a[i] == a[j])j++;//因为j从第2个数开始遍历,最极端j遍历到倒数第二个数也应该把所有非所求数标记完成了
//3 5 5 5 3 3 3 和 5 5 5 3 3 3 3 感受一下
if (j != N && a[i] != a[j])i++,dp[j++] = 1;
else break;
}
while (dp[i])i++;
printf("%d\n", a[i]);
}
return 0;
}
经典入门题一:HDU 2084 数塔
本题在紫书上称数学三角问题。如果直接从上往下DFS肯定是超时的,因为在上层的数被重复计算的次数可能会特别多。每个数往下走有两个方向,比如第一个数要被每种可能的结果加一次,如果数塔是n层,那么被计算的次数为2^n-1次,当n比较大的时候,指数的力量是强大的。
一开始我看到这个题目的想法是从上往下每次选数大的。虽然显然直接这么做不可行,但是如果每一个数往下走的两个方向你都知道了最优的结果,那么这就变成可行的了。
一层一层往下推,变成子问题,发现问题的解法都是相同的,而当到倒数第二层,只需比较最后一层的两个数即可,这里就是边界了。
因此可以从下往上依次知道每条路径的最优结果,上层数选择两个路径中更优的一个继续去比较直到最顶层得到最优解。
在我看来,这样的递推的想法就是最关键的。
代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100 + 5;
int C, N, a[maxn][maxn], dp[maxn][maxn];
int main(void)
{
scanf("%d", &C);
while (C--)
{
scanf("%d", &N);
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= N; i++)
for (int j = 1; j <= i; j++)
scanf("%d", &a[i][j]);//i表示第i层,j表示该层的第j个数
//在dp数组中i,j也表状态即从第i层第j个数往下的最优选择结果这个状态
for (int j = 1; j <= N; j++)
dp[N][j] = a[N][j];//最后一层没有往下的数字,无需选择最优路径
for (int i = N - 1; i >= 1; i--)
for (int j = 1; j <= i; j++)
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + a[i][j];//在第i层第j个数的下面两天路径最优结果已经被存入dp数组中
printf("%d\n", dp[1][1]); //直接选择最优路径加上本身的数即可
}
return 0;
}
动态规划的核心是状态和状态转移方程
经典入门题二:POJ 2533 Longest Ordered Subsequence
最长上升子序问题(LIS)
从数的开头往后遍历,对于每个数的问题就是在不在子序当中的问题,如果选了这个数,子序会怎么变化,如果没选子序会怎么变化。这里写的是最简单的时间复杂度O(n^2)的方法,主要是能体现DP。
代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 1000 + 5;
int N, a[maxn], dp[maxn], ans;
int main(void)
{
scanf("%d", &N);
for (int i = 1; i <= N; i++)
{
scanf("%d", &a[i]);
dp[i] = 1;//dp[i]最小的情况
for (int j = 1; j < i; j++)
if (a[j] < a[i])//dp[i]只考虑第i个数入选子序列,因此要求第j个数严格小于第i个数
dp[i] = max(dp[i], dp[j] + 1);
ans = max(ans, dp[i]);//查看第i种状态是否更优
}
printf("%d\n", ans);
return 0;
}
经典入门题三:POJ 1458 Common Subsequence
最长公共子序问题(LCS)
有时候把问题想简单了,直接选取一个序列从头到尾遍历,如果第二个序列按照顺序出现了相同字母,则入选子序列,这样即可自我感觉良好地得到错误答案。
因为跳过该字母选取后面的字母可能会得到更长的公共子序列。如abcfcb和abfcab,遍历第一个序列按顺序选a、b、c、b得到子序列abcb长度为4,但是如果跳过c,将f入选,则结果为abfcb长度为5,也就是说如果有相同字母出现,选或是不选可能会影响后续的结果,这种现象就得用动态规划来解决。
一般推方程都是要从小往大推,这里我要借鉴一下别人通过列表推状态转移方程(有时候发现列表推方程挺好,但我目前还不能明确什么时候该去列表)
下标(j) | 1 | 2 | 3 | 4 | 5 | 6 | ||
---|---|---|---|---|---|---|---|---|
s2 | a | b | f | c | a | b | ||
下标(i) | s1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | a | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | b | 0 | 1 | 2 | 2 | 2 | 2 | 2 |
3 | c | 0 | 1 | 2 | 2 | 3 | 3 | 3 |
4 | f | 0 | 1 | 2 | 3 | 3 | 3 | 3 |
5 | c | 0 | 1 | 2 | 3 | 4 | 4 | 4 |
6 | b | 0 | 1 | 2 | 3 | 4 | 4 | 5 |
上面表格的意思是随着两个字母序后续字母的增添得出当前状态的最长子序列的长度,只有一个序列时,长度为0即初始状态。
序列为a和a时长度为1,序列为ab和a时长度为1,序列为ab和ab时长度为2以此类推。
在每次推当前序列的时候都可以借用一下之前推出的结论,例如开始(1,1),(1,2),(2,1)都可以直接得到1,在推(2,2)时发现:末尾两个字母相等,将其入选子序列,则长度=dp[1][1]+1,即得出该种情况dp[i][j]=dp[i-1][j-1]+1,也就是长度为i-1和j-1的最长公共子序列已经知道长度了,比较长度为i和j的序列,那么最长子序列的长度是前者+1即可,这就是当前状态最好的情况。
如果末位两个字母不等,如(5,3)为abcfc和abf,则必然不可能为i-1和j-1序列得出的最长子序列长度+1,这时选长度为i-1和长度为j的序列比较得出最长子序列的长度①,如abcf与abf比较得出abf的长度3,长度为i和长度为j-1的序列比较得出最长子序列的长度②,如abcfc与ab比较得出ab的长度2,显然i-1和j-1比较得出的结果必定小于等于前两者中任意一个,可以不纳入考虑。dp[i][j]=max{dp[i-1][j],dp[i][j-1]}。
本题的边界可以看成是两个序列中某一个长度为0的情况,此时最长公共子序列长度为0。
代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
int dp[10000][10000];
char a[10000], b[10000];
int main(void)
{
while (scanf("%s%s", a + 1, b + 1) == 2)
{
int len1 = strlen(a + 1);
int len2 = strlen(b + 1);
for (int i = 1; i <= len1; i++)
for (int j = 1; j <= len2; j++)
if (a[i] == b[j])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
printf("%d\n", dp[len1][len2]);
}
return 0;
}
经典入门题四:01背包 HihoCoder - 1038
题目链接自带优质题解
代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 500 + 5;
const int maxm = 1e5 + 5;
int N, M, dp[maxn][maxm];
int main(void)
{
scanf("%d%d", &N, &M);
for(int i=1;i<=N;i++)
{
int need, value;
scanf("%d%d", &need, &value);
for (int j = 1; j <= M; j++)
if (j >= need)//假设前i-1件礼品的选择已经得到了最好的结果,判断换取第i件奖品是否能有更好的结果
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - need] + value);
else dp[i][j] = dp[i - 1][j];
}
printf("%d\n", dp[N][M]);
return 0;
}
经典入门题五:完全背包 HihoCoder - 1043
题目链接自带优质题解
代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxm = 1e5 + 5;
int N, M, dp[maxm];
int main(void)
{
scanf("%d%d", &N, &M);
for(int i=1;i<=N;i++)
{
int need, value;
scanf("%d%d", &need, &value);
for (int j = need; j <= M; j++)//大容量的结果由小容量递推得出,如果j<need,dp[j]保留位原值
dp[j] = max(dp[j], dp[j - need] + value);
}
printf("%d\n", dp[M]);
return 0;
}