学习算法已经有1年半左右了。学算法刷题必不可少,刚开始的时候遇到题出不了只是坐那苦想,然后某一天得知上网可以搜到解题报告,兴冲冲打开网页,在百度搜索框里粘贴了题目名字,回车,确实有各种题解。当时年少不懂网络的强大还感叹了一把。点开之后发现,几乎所有解题报告都是以个人博客的名义贴出来的,原创的有,不过雷同的居多。忽略这些细节,当时确实很有一种自己什么时候也能写博客的想法。毕竟人都是希望自己上进的,此外,也算是自己以后偶尔回忆过去的原材料。
学校能有ACM集训队伍,也算是我的幸运。刚开始报名是干劲十足的,很曲折地坚持着进去了才发现自己确实很有不足,在学校范围内尚且如此,放到了外面则更是渺小。基本算法的学习只一年不到教练就讲完了。基本上只是带入门,资质略浅的我听得根本就是云里雾里,然后专题也是靠着题解的理解水了一些,然后这半年多就这么过去了。期间零零散散又补了一些知识,不过还是不够深入,我想,写博客这种东西或许能带动思维,加深理解。
说到动态规划,最开始接触到这类型的题目是在教练上课的时候放了杭电OJ的名为“数塔”的题目,其实仅靠着没有任何算法基础而且对电脑编程处理问题的方式还不熟悉只会暴力加模拟的思维方式,着实没有除开暴力之外的其它方法。所以即使是现在看来这样简单的题目,也能难住很多初学者,并且DP确实很灵活,而且不好理解。不过万事开头难,理解了一些题目,慢慢也能看懂题解并自己做题了。经过我自己这段时间的捣鼓,总结了自己的一些东西。
动态规划是算法里面比较特殊的一类,没有特定的模板,没有特定的知识点,能说的方法也很少。总的来说,动态规划类题目有两个特点:一是问题包含最优子结构;二是子状态可以重复取到。和搜索一样,是计算机擅长而人不擅长的处理问题的方式。所以一开始理解起来确实多有不便。说回来,应这两个特点,动态规划的要素有二:一是状态方式的选取;二是状态转移方程或转化公式。其中前者是关键,也是难点,因为状态选出来了后者不难得到的,甚至有时候能想出前者是因为先想出了后者,并且想出每一个方案都得再想怎样把每个状态都循环取到,并证明是否具有最优子结构性质……所以经过这么些题目的“洗礼”,我算是摸到了其冰山一角,记下一些这些天来的训练题目与思考方式。
hduOJ2084 数塔:http://acm.hdu.edu.cn/showproblem.php?pid=2084
核心代码:
for (int i = n - 2; i >= 0; i--)
{
for (int j = 0; j <= i; j++)
{
dp[i][j] = max(a[i][j] + dp[i + 1][j], a[i][j] + dp[i + 1][j + 1]); /*子状态由所有可以到达此状态的上一状态更新*/
}
}
答案即是数塔最顶端dp[0][0],这种可以很容易得到状态表示的动态规划算是最简单入门的一种了,而且本题还是直接给出了提示性的数据结构,只要能有最优子结构的概念就能出。
hduOJ1176 免费馅饼:http://acm.hdu.edu.cn/showproblem.php?pid=1176
核心代码:
for (int i = T - 1; i >= 0; i--)
{
for (int j = 0; j <= 10; j++) /*下面是子状态由所有可以到达此状态的上一状态的更新过程*/
{
if (j != 0)
dp[i][j] = max(dp[i][j], dp[i + 1][j - 1] + value[i + 1][j - 1]);
if (value[i + 1][j])
dp[i][j] = max(dp[i][j], dp[i + 1][j] + value[i + 1][j]);
if (j != 10)
dp[i][j] = max(dp[i][j], dp[i + 1][j + 1] + value[i + 1][j + 1]);
}
}
答案为dp[0][5],即最开始坐标为5位置的答案,这一题和上一题一样,都是逆序枚举,可以保证最后结果不用再选。这依然是可以比较容易得出状态表示的一种,因为可以比较容易画出它的状态转换DAG(有向无环)图,同一状态有多种不同方式到达,典型的需要转化公式的动态规划题。
hduOJ1003 Max Sum:http://acm.hdu.edu.cn/showproblem.php?pid=1003
核心代码:
for (i = 1; i < n; i++)
{
if (a[i] + sum >= a[i])
{
sum = a[i] + sum;
}
else
{
sum = a[i];
b = i;
}
if (sum > maxsum)
{
maxsum = sum;
end = i;
beg = b;
}
}
一开始我不知道这是动态规划类的题目,只是觉得方法比较巧妙,因为学习用课件上的版本,这一句
if (a[i] + sum >= a[i])
{
sum = a[i] + sum;
}
被简化成了
if (sum >= 0)
{
sum = a[i] + sum;
}
因而看不出动态规划标志性的具有转移方程或转化公式的特点,重做这道题的时候自然而然写出了简化之前的代码。思路就是留下前面对后续累加有帮助的串,即加上之后比没加上大,若到某位置此条件不成立,则另起起点。同时要注意,最终的最大值可能在任何两起点之间产生。
hduOJ1159 Common Subsequence:http://acm.hdu.edu.cn/showproblem.php?pid=1159
核心代码:
for (int i = 0; i < a.length(); i++)
{
for (int j = 0; j < b.length(); j++)
{
if (a[i] == b[j])
dp[i + 1][j + 1] = dp[i][j] + 1; /*串1与串2当前位置字符相等,子状态可以更新*/
else
dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]); /*串1与串2当前位置字符不相等,子状态沿用可以到达次状态的最值*/
}
}
这个状态表示方式若还有意用头两个题一样的思维方式的话会发现很难得出,因为它的状态不再单单是以某一步为下标的DAG图,而是以它自身子结构的状态。要算dp[I][J],可以从i:0->I,j:0->J,依次求出其子结构的最值,下一个子结构的最值则可以很方便由前面已知的子结构最值得到。
hduOJ2602 Bone Collector:http://acm.hdu.edu.cn/showproblem.php?pid=2602
核心代码:
for (int i = 0; i < n; i++) /*这样遍历可以保证每个val[i]对每个dp[j]能且仅能入选一次*/
{
for (int j = maxkg; j >= kg[i]; j--) /*dp[j]表示总量为j时的最值*/
{
dp[j] = max(dp[j - kg[i]] + val[i], dp[j]); /*更新状态*/
}
}
这个状态的表示方法与上题比较类似,要求最终结果,先求其子结构,但是又多了另一种方式的转移方程,即更新dp[j]时不一定只用到的是上一个状态。
hduOJ1203 I need a offer:http://acm.hdu.edu.cn/showproblem.php?pid=1203
核心代码:
for (i = 0; i < m; i++) /*对于每个mon[i]有选与不选的自由*/
{
for (int j = n; j>=mon[i]; j--) /*dp[j]表示总量为j时的最值*/
{
dp[j] = min(dp[j], dp[j - mon[i]] * pro[i]); /*更新状态*/
}
}
与上题是一个类型,只是转移方程略有不同。其实这类题归结于0-1背包,是代码简化后的版本。上题有说”保证每个val[i]对每个dp[j]能且仅能入选一次“,是与每个val[i]有无限个可选类的题目区分开。