以前对动规的理解太浅了,总是直接把当前状态保存,然后下一次遍历状态,然后更新状态。这是基于之前写 指数次循环的解法里总结的。
于是就对一些动规的题都直接这样套,造成的问题就是,如果迭代时每个子状态再生成N个子状态,那么要存储的变量就太多太大了!会造成时间和空间上的双重问题。
正确的思路应该是根据状态的属性寻找状态空间,寻找重复状态!确定状态量属性范围,确定遍历方式,确定状态前后的转移关系。
实例
看LeetCode 72. Edit Distance:
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入: word1 = “horse”, word2 = “ros”
输出: 3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
一开始我想岔了,以为需要先找最长重复子序列,后来发现直接dp就行,然后先写了递归的版本。然后TLE了。
于是意识到应该是递归函数栈调用超时。开始递归转循环,于是就要找循环的遍历范围,状态空间是word1的每一个位置和word2的每一个位置对应一个状态量最小操作数。遍历方式就两个嵌套循环。状态转移关系与递归类似的。
然后又在discussion里找到了一种空间使用更少的dp方案。把外循环的量只用一个临时变量存下。
代码:
class Solution {
public:
bool recursive(string word1, string word2, int num, int& minc) {
if (word1 == word2) {
minc = num < minc ? num : minc;
return true;
}
if(word2.size() < 1) {
num += word1.size(); // delete n
minc = num < minc ? num : minc;
return true;
}
if (word1.size() < 1) { // word2.size() != 0
num += word2.size(); // insert n
minc = num < minc ? num : minc;
return true;
}
// 一种加速方案,过滤掉比之前方案还差的
if (num > minc) {
return false;
}
if (word1[0] == word2[0]) {
recursive (word1.substr(1, word1.size()-1), word2.substr(1, word2.size()-1), num, minc);
} else {
/// delete
recursive (word1.substr(1, word1.size()-1), word2, num+1, minc);
/// replace
recursive (word1.substr(1, word1.size()-1), word2.substr(1, word2.size()-1), num+1, minc);
/// insert
recursive (word1, word2.substr(1, word2.size()-1), num+1, minc);
}
return true;
}
int dpfind(string word1, string word2) {
// 循环的dp,
// 每次三种选择,需要储存每种的选择的位置和操作总次数。这样就3^n了。。。
// 状态量还与word的位置有关,word1 的每一个位置对应word2的每一个位置,就包含了所有状态量,
// 寻找重复状态!确定状态量属性范围,确定遍历方式,确定状态转移关系。
int **dp;
dp = new int* [word1.size()+1];
for (int i = 0; i < word1.size()+1; ++i) {
dp[i] = new int [word2.size()+1];
}
dp[0][0] = 0; // 序号==原长度-剩余长度
for (int i = 0; i < word1.size()+1; ++i) {
dp[i][0] = i;
}
for (int i = 0; i < word2.size()+1; ++i) {
dp[0][i] = i;
}
for (int i = 1; i < word1.size()+1; ++i)
for (int j = 1; j < word2.size()+1; ++j) {
if (word1[i-1] == word2[j-1]) {
dp[i][j] = dp[i-1][j-1];
} else {
// 上一次的 delete
int de = dp[i-1][j]+1;
// 上一次的 replace
int re = dp[i-1][j-1] +1;
// 上一次的 insert
int ie = dp[i][j-1] +1;
dp[i][j] = min(min(de, re),ie);
}
}
return dp[word1.size()][word2.size()];
}
// dicussing 更少存储的一种dp
int dpfindmin(string word1, string word2) {
int m = word1.length(), n = word2.length();
vector<int> cur(m + 1, 0);
for (int i = 1; i <= m; i++)
cur[i] = i;
for (int j = 1; j <= n; j++) {
int pre = cur[0];
cur[0] = j;
for (int i = 1; i <= m; i++) {
int temp = cur[i];
if (word1[i - 1] == word2[j - 1])
cur[i] = pre;
else cur[i] = min(pre + 1, min(cur[i] + 1, cur[i - 1] + 1));
pre = temp;
}
}
return cur[m];
}
int minDistance(string word1, string word2) {
/// 1. find longest subsequece ..balbla
/// 2. dp. try all
/// max = dist(length1-length2) + length2
/// find longest is wrong. dp
/// dp: 1.try a.replace it with same one
/// 2. try remove it
/// 3.insert same one
/// 4. if pre is same, 1 2 3 可以略过
/// 先写递归,再写循环版的
int ans = INT_MAX;
//recursive (word1, word2, 0, ans);
ans = dpfind(word1,word2);
return ans;
}
};
用循环写递归还要多加练习,尤其是这种要寻找dp的状态空间范围的。