题意
给定一个英文单词,用两只手指去码字。每只手指移动所需要的代价为键盘上两字母之间的距离,求码完整个单词所需要的最小代价。
解法
最直观的想法是,运用 d p [ i d x ] [ p o s 1 ] [ p o s 2 ] dp[idx][pos1][pos2] dp[idx][pos1][pos2]来表示输入到第 i d x idx idx个字母,并且第一只手指在 p o s 1 pos1 pos1,第二只手指在 p o s 2 pos2 pos2时的最小代价。这时状态转移方程为:
- 移动左手: d p [ i d x + 1 ] [ c u r ] [ o t h e r ] = m i n ( d p [ i d x + 1 ] [ c u r ] [ o t h e r ] , d p [ i d x ] [ p r e v ] [ o t h e r ] + c o s t ) ; dp[idx+1][cur][other] = min(dp[idx+1][cur][other], dp[idx][prev][other] + cost); dp[idx+1][cur][other]=min(dp[idx+1][cur][other],dp[idx][prev][other]+cost);
- 移动右手: d p [ i d x + 1 ] [ o t h e r ] [ c u r ] = m i n ( d p [ i d x + 1 ] [ o t h e r ] [ c u r ] , d p [ i d x ] [ o t h e r ] [ p r e v ] + c o s t ) ; dp[idx+1][other][cur] = min(dp[idx+1][other][cur], dp[idx][other][prev] + cost); dp[idx+1][other][cur]=min(dp[idx+1][other][cur],dp[idx][other][prev]+cost);
由于每个状态都是从上个状态转移过来的,通过优化,我们可以把第一维度优化掉,变成 d p [ 2 ] [ p o s 1 ] [ p o s 2 ] dp[2][pos1][pos2] dp[2][pos1][pos2]这个形式。这种方法的时间复杂度没变,为 O ( w o r d L e n g t h ∗ 26 ∗ 26 ) O(wordLength*26*26) O(wordLength∗26∗26)。
仔细观察后,我们发现区分左右手的其实是没有必要的,比如 d p [ 2 ] [ p o s 1 ] [ p o s 2 ] dp[2][pos1][pos2] dp[2][pos1][pos2]和 d p [ 2 ] [ p o s 2 ] [ p o s 1 ] dp[2][pos2][pos1] dp[2][pos2][pos1]是一样的,因为我们可以想象随时左右手交换,结果也不会改变。同时,一个更重要的发现是,由于我们是按顺序码字,因此每次总有一只手停留在上一个刚码字母的那个字母处。
有了这个信息,我们可以只记录后手所在的位置(因为先手一定落在刚刚敲击完字母的那个位置),来进行动态规划。我们用 d p [ i d x ] [ x ] dp[idx][x] dp[idx][x]表示先手在刚刚敲击完 w o r d [ i d x ] word[idx] word[idx],后手停留在字母 x x x处的最小代价。于是状态转移方程变为:
- 移动先手: d p [ i d x ] [ t ] = m i n ( d p [ i d x ] [ t ] , d p [ i d x − 1 ] [ t ] + c o s t [ a 1 ] [ a 2 ] ) ; dp[idx][t] = min(dp[idx][t], dp[idx-1][t] + cost[a1][a2]); dp[idx][t]=min(dp[idx][t],dp[idx−1][t]+cost[a1][a2]);
- 移动后手: d p [ i d x ] [ a 1 ] = m i n ( d p [ i d x ] [ a 1 ] , d p [ i d x − 1 ] [ t ] + c o s t [ t ] [ a 2 ] ) ; dp[idx][a1] = min(dp[idx][a1], dp[idx-1][t] + cost[t][a2]); dp[idx][a1]=min(dp[idx][a1],dp[idx−1][t]+cost[t][a2]);
这样一来,时间复杂度便可优化到 O ( w o r d L e n g t h ∗ 26 ) O(wordLength*26) O(wordLength∗26)。同理,我们可以优化掉第一维空间,使得最终的空间复杂度为常数级别。
思考
对于此类题型,我们如果发现以下两个特征:
- 多维DP数组填不满或者有冗余(左右顺序交换不太重要,比如 d p [ p o s 1 ] [ p o s 2 ] dp[pos1][pos2] dp[pos1][pos2]等同于 d p [ p o s 2 ] [ p o s 1 ] dp[pos2][pos1] dp[pos2][pos1])
- 多维DP数组里不同维度的值加起来等于一个恒定值(比如 p o s 1 + p o s 2 = i d x pos1 + pos2 = idx pos1+pos2=idx)
这种情况,可以去想着通过优化空间得到更优解。
代码
const int INF = 0x3f3f3f3f;
class Solution {
public:
int dp[2][27];
vector<vector<int>> cost;
pair<int, int> getPos(int x) {
return make_pair(x/6, x%6);
}
int calculateDis(pair<int, int> pos1, pair<int, int> pos2) {
return abs(pos1.first-pos2.first) + abs(pos1.second-pos2.second);
}
int minimumDistance(string word) {
cost = vector<vector<int>>(30, vector<int>(30, 0));
for (int i=0; i<26; i++)
for (int j=0; j<=i; j++) {
pair<int, int> pos1 = getPos(i);
pair<int, int> pos2 = getPos(j);
cost[i][j] = cost[j][i] = calculateDis(pos1, pos2);
}
memset(dp, 0x3f, sizeof(dp));
for (int i=0; i<26; i++)
dp[0][i] = 0;
int len = word.length();
for (int idx=1; idx<len; idx++) {
int cur = idx&1, prev = !cur;
int a1 = word[idx-1] - 'A', a2 = word[idx] - 'A';
for (int t=0; t<26; t++) if (dp[prev][t] != INF) {
dp[cur][t] = min(dp[cur][t], dp[prev][t] + cost[a1][a2]);
dp[cur][a1] = min(dp[cur][a1], dp[prev][t] + cost[t][a2]);
}
for (int i=0; i<26; i++)
dp[prev][i] = INF;
}
int ans = INF;
for (int i=0; i<26; i++)
ans = min(ans, dp[(len-1)&1][i]);
return ans;
}
};
相关题型
题目:
给定一个类似双端队列的数组
a
r
r
arr
arr,每次取元素只能从两头取,但是读此数组中的元素可以读任意位置上的。再给定一个数
K
K
K,以及长度为K的
m
u
l
mul
mul数组,要求从
a
r
r
arr
arr里取
K
K
K个元素,与这个
m
u
l
mul
mul的元素从左到右依次相乘,求乘积和最大为多少?
思路:
这个题还是可以开一个
d
p
[
s
t
e
p
]
[
p
o
s
1
]
[
p
o
s
2
]
dp[step][pos1][pos2]
dp[step][pos1][pos2]的数组,代表总共取了step个数字,其中数组左边取了
p
o
s
1
pos1
pos1个元素,数组右边取了
p
o
s
2
pos2
pos2个元素时的最优乘积。
然而,注意到我们可以利用类似上个题空间优化的思想,用dp[step][x]表示 目前已经取了 s t e p step step个数字,并且这 s t e p step step个数字里包含左端 x x x个数字的最优乘积。得到以下状态转移方程:
d p [ s t e p ] [ x ] = m a x ( d p [ s t e p − 1 ] [ x − 1 ] + a r r R i g h t [ x ] ∗ m u l [ s t e p ] , d p [ s t e p − 1 ] [ x ] + a r r L e f t [ s t e p − x ] ∗ m u l [ s t e p ] ) ) dp[step][x] = max(dp[step-1][x-1] + arrRight[x]*mul[step], dp[step-1][x] + arrLeft[step-x]*mul[step])) dp[step][x]=max(dp[step−1][x−1]+arrRight[x]∗mul[step],dp[step−1][x]+arrLeft[step−x]∗mul[step]))
接着同理,优化掉第一维度数组,自底向上DP求解即可。这里时间复杂度没变,为 O ( K 2 ) O(K^2) O(K2),但空间复杂度优化到了 O ( K ) O(K) O(K)。
参考文献
https://leetcode.com/problems/minimum-distance-to-type-a-word-using-two-fingers/discuss/477652/JavaC%2B%2BPython-1D-DP-O(1)-Space