题解正文
题目描述
问题分析
此题给出两个数字数组,然后使用这两个数组中的数字组成一个数字记为Res,要求Res中来自同一个数组的数字保持在原数组中的顺序,然后求出符合规则的最大Res(对应的数字串)。
解题思路
这个题目做法思路很清晰,我们遍历i,i∈[0,k),然后将问题分成两个步骤:
- 从数组nums1/nums2中挑选出i/k-i个数,组成一个数组,其中元素保持原来的相对顺序不变,使该数组中元素按序组成的数字最大化,比如数组{5,1,2,3}中选出两个数,我们选择{5,3},因为它里面的元素按序组成的数字53是符合要求的数字中最大的;
- 保持元素相对顺序不变前提下,将数组nums1和nums2合并,使合并后的数组中元素按序组成的数字最大化,记该最大数字为Res[i],比如我们将数组{5,3}和{4,6}合并为{5,4,6,3},因为合并后数组中元素按序组成的数字5463是符合要求的数字中最大的;
然后选出对应Res[i]最大的那个数组作为最终答案(一共有k个数组,选出Res[i]最大的作为答案);
上面第二个步骤很简单,只需要不断从两个数组首元素中选出较大的数字加入到答案数组中即可,比如{5,3}和{4,6}两个数组,首先选出5加入答案数组,因为这是两个数组中首元素较大者,两个数组变为{3}和{4,6},然后同理选出4,两个数组变成{3}和{6},然后同理选出6,两个数组变为{3}和{},最后选出3,两个数组都为空,结束合并
第一个步骤就比较麻烦了,
建议写这个题目之前先写Remove Duplicate Letters,这样可能带来一些启发,关于这个题目我也写过一篇博客:传送门;
为什么要先完成Remove Duplicate Letters呢,虽然这两道题不同,但是Remove Duplicate Letters能够给该题的第一个步骤提供一个思路:
同样是按序、按要求选出最优字串,我们可以这样做:
- 每次选出一个符合条件的最优元素;
- 我们需要先确定符合条件的元素可存在范围;
- 然后从符合要求的元素范围中选择最优的;
所以对于这个题的第一个步骤:
我们从数组nums1/nums2中挑选出num=i/k-i个数,记length=length1/length2是nums1/nums2数组长度,然后第一个符合要求的数字范围是[0,length+1-num),因为我们不能够选下标从length+1-num到length-1的这些数字,如果选了那么要从该数组中截掉前面数字(从而保证有序),这将导致剩余可供选择的数字不足num-1个,没办法选出符合要求的数组;
然后我们从[0,length+1-num)中选择最大的数字加入步骤一的结果数组,记这个最大数字的下标为index,那么数组的前index+1个元素要被去除,以保证有序且不重复(第index+1个数就是该步选出的数,为保证不重复也要去掉);
将num更新为num-1,表示还需要从后续数组选出num-1个数(因为已经选了一个了),length更新为length-index-1,表示当前数组被截取之后长度为length-index-1,然后重复上面的两个步骤,直到需要从数组nums1/nums2中挑选出i/k-i个数已经全部选出来,步骤1结束;
综合上面俩个步骤的操作,我们可以得到这个题的解法。
但是即使这样做能得到答案,算法复杂度也是无法接受的,首先第一个步骤的复杂度是O(kmin{m,n})级别的:外层遍历k个需要选择的数字,内层每选出一个数字需要遍历规模O(n/m)的数组找出最优解,所以复杂度是O(kmin{m,n}),第二个步骤的归并操作则只需要O(m+n),但是在最外层我们需要遍历k种选择策略(i个数字从第一个数组选,k-i个数字从第二个数组选),所以要找出最优解需要做k个复杂度O(kmin{m,n})的操作,于是最终复杂度为O(KKmin{m,n}),这差不多是立方级别复杂度,按照这个立方级别复杂度的算法做直接超时,所以肯定有更好的做法;
考虑到每次选出最优解的时候总要遍历数组1或者数组2,那么可以提前算出max1[from][to]和max2[from][to]分别表示从from到to的数字中最大的数字,它们的范围是0到length1/length2,然后后续求某个连续范围的最优解就可以通过O(1)复杂度得到答案。
而这个max[from][to]数组通过动态规划容易求得:
max[from][to]=
-1, 当from > to;
nums[from]=nums[to], 当from = to;
max[from][to-1]和nums[to]中较大的,当from < to;
max[i][j]一共length^2个,每个max[i][j]求解最多只需要进行一次比较和赋值,这个记忆化操作复杂度是平方级别的;
完成记忆化之后再进行求解:
最外层遍历k种选择策略(i个数字从第一个数组选,k-i个数字从第二个数组选),求解最优的数字串
然后是两次遍历分别求出两个数组的结果字串,这需要
O
(
2
×
n
)
O(2 \times n)
O(2×n)的复杂度,因为一共选出m/n个元素,每个元素选择就是在数组1/数组2的某一连续字串中选择最大的数字,即求解max[from][to],from表示该元素符合要求的开始范围,to表示符合要求的结束范围,由于前面的记忆化已经求出max[i][j]数组,故可通过常数时间求得,所以这两次遍历一共需要
O
(
m
+
n
)
O(m+n)
O(m+n)的复杂度。
然后就是合并两个选出的数组,其复杂度为O(m+n)。
综合记忆化操作和求解操作,我们得到的算法复杂度是 O ( 2 ( m + n ) × k + m 2 + n 2 ) = O ( m a x ( m , n , k ) 2 ) O(2(m+n) \times k+m^2+n^2)=O(max(m,n,k)^2) O(2(m+n)×k+m2+n2)=O(max(m,n,k)2),这个平方级别复杂度能够保证评测通过。(通过动态规划将算法从立方复杂度优化到平方复杂度)
关于这个算法的正确性很容易证明,假设数字串
x
1
x
2
x
3
.
.
.
x
k
x_1x_2x_3...x_k
x1x2x3...xk是按照题目要求从分别从两个数组中取出来的数字并按照原来顺序组成的串,其中i个从第一个数组中取,k-i个从第二个数组中取,它对应子串
x
a
1
x
a
2
x
a
3
.
.
.
x
a
i
x_{a_1}x_{a_2}x_{a_3}...x_{a_i}
xa1xa2xa3...xai和
x
b
1
x
b
2
x
b
3
.
.
.
x
b
k
−
i
x_{b_1}x_{b_2}x_{b_3}...x_{b_{k-i}}
xb1xb2xb3...xbk−i
那么对于在第一个数组中i个数的选择,按照步骤1算法可以得出最优解:假设有其它不同于步骤一算法的选择
x
c
1
x
c
2
x
c
3
.
.
.
x
c
i
x_{c_1}x_{c_2}x_{c_3}...x_{c_i}
xc1xc2xc3...xci,那么在第一个出现不同选择的位置j,j∈[0,i)上就有
x
c
j
<
x
a
j
x_{c_j} < x_{a_j}
xcj<xaj,因为前j-1个选择相同,所以截取出的可选剩余数组相同,在这种情况下得到的第j步可选元素范围自然也相同,而
x
a
i
x_{a_i}
xai是该范围中最大的,因此
x
c
j
<
x
a
j
x_{c_j} < x_{a_j}
xcj<xaj,由于在高位出现大小不同,低位无需再做比较就能得知,
x
a
1
x
a
2
x
a
3
.
.
.
x
a
i
x_{a_1}x_{a_2}x_{a_3}...x_{a_i}
xa1xa2xa3...xai是更好的选择。因此
x
a
1
x
a
2
x
a
3
.
.
.
x
a
i
x_{a_1}x_{a_2}x_{a_3}...x_{a_i}
xa1xa2xa3...xai是最优选择。
对于第二个数组中k-i个数字的选择自然也是能够用步骤一算法得出最优解。
最后就是说明为何按照步骤二组合能够得到最优解:我们每一次都选择两个数组中头元素最大的元素加入答案,这总比加入更小的那一个好,因为如果加入更小的那个,就会在这一位导致得不到最大值,低位不用比较,这个数就不是最大的。
然后从k种选择方法里面选择最好的一种,就能得到最优解。
综上,算法正确。
算法步骤
初始化结果数组Res[k],其中全部元素值为0;
遍历k种选择方法:i个元素从第一个数组中选择,k-i个元素从第二个数组中选择,i∈[0,k]
初始化max1,max2数组:
如果from > to,max[from][to]=-1;
如果max[from][to]=nums[from]=nums[to];
如果当from < to,max[from][to]=max[from][to-1]和nums[to]中较大的;
从第一/二个数组中选出i/k-i个最优选择元素:
初始化k=i,pos=0;
当k>0,执行如下循环
将max[pos][length-k]加入到选择结果中;
更新k=k-1;
找到第一个nums[index]=max[pos][length-k],将pos更新为index+1;
合并两个选出来的数组:
每次比较两个选出的数组的首个元素,将较大的加入到结果数组,直到两个数组均为空,得到该层循环的结果数组;
将该层循环得出的结果数组按序组成的数字与Res[k]中元素组成的数字比较,如果大于Res[k]中元素组成的数字,
则将Res[k]数组中的元素替换为该层循环得出的结果数组;
返回Res[k]结果数组
复杂度分析
前面的解题步骤中已经详细说明了复杂度优化过程,从中我们知道我们的算法时间复杂度是
O
(
(
m
+
n
)
×
k
)
O((m+n) \times k)
O((m+n)×k),是平方级别的复杂度;
对于空间复杂度,一共用了两个二维数组和其他常数个数的变量(相对可以忽略),所以空间复杂度是
O
(
m
2
+
n
2
)
O(m^2+n^2)
O(m2+n2),也就是平方级别的;
代码实现&结果分析
代码实现:(100多行的算法题确实也是难得了orz)
class Solution {
public:
void init (int** & max1, int length1, vector<int>& nums1) {
max1 = new int* [length1];
for (int i = 0; i < length1; ++i) {
max1[i] = new int [length1];
}
for (int i = 0; i < length1; ++i) {
for (int j = 0; j < length1; ++j) {
if (i > j) max1[i][j] = -1;
else if (i == j) max1[i][j] = nums1[i];
else {
max1[i][j] = nums1[j] > max1[i][j-1] ? nums1[j] : max1[i][j-1];
}
}
}
}
vector<int> maxList(int** & max1, int length1, vector<int> & nums1, int k) {
vector<int> res;
int pos = 0;
while (k > 0) {
int temp = max1[pos][length1-k];
res.push_back(temp);
k--;
while (nums1[pos] != temp) pos++;
pos++;
}
return res;
}
int chooseMaxList(vector<int>& maxList1, int index1, vector<int>& maxList2, int index2) {
int length1 = maxList1.size();
int length2 = maxList2.size();
while (index1 < length1 && index2 < length2) {
if (maxList1[index1] > maxList2[index2]) return 1;
else if (maxList1[index1] < maxList2[index2]) return 2;
else {
index1++;
index2++;
}
}
if (index1 < length1) {
return 1;
} else {
return 2;
}
}
vector<int> maxNumber(vector<int>& nums1, vector<int>& nums2, int k) {
vector<int> res(k);
for (int i = 0; i < k; ++i) {
res[i] = 0;
}
int length1 = nums1.size();
int length2 = nums2.size();
int ** max1;
int ** max2;
init(max1, length1, nums1);
init(max2, length2, nums2);
int start = k - length2 > 0 ? k-length2 : 0;
int end = k - length1 > 0 ? length1 : k;
for (int i = start; i <= end; ++i) {
int flg = 0;
vector<int> maxList1 = maxList(max1, length1, nums1, i);
vector<int> maxList2 = maxList(max2, length2, nums2, k-i);
int index1 = 0, index2 = 0;
for (int j = 0; j < k; ++j) {
int head = 0;
if (index1 < i && index2 < k-i) {
if (maxList1[index1] > maxList2[index2]) {
head = maxList1[index1];
index1++;
} else if (maxList1[index1] < maxList2[index2]) {
head = maxList2[index2];
index2++;
} else if (chooseMaxList(maxList1, index1+1, maxList2, index2+1) == 1) {
head = maxList1[index1];
index1++;
} else {
head = maxList2[index2];
index2++;
}
} else if (index1 < i) {
head = maxList1[index1];
index1++;
} else {
head = maxList2[index2];
index2++;
}
if (flg == 1) {
res[j] = head;
continue;
}
if (res[j] > head) break;
else if (res[j] == head) continue;
else {
flg = 1;
res[j] = head;
}
}
}
return res;
}
};
提交结果:(从提交结果看起来效果一般般,应该是vector操作用的太多或者其它细节原因,从复杂度分析来看O(k*(m+n))的复杂度已经差不多是最好的了,从评论区来看最好的算法也是这个级别的复杂度,更好的做法应该仅仅是多了一些细节的优化)
心得体会
这周上来就先把这个题目完成,算是完成上周立的flag,然后再去考虑其它题目或者整理书本习题,这周应该还会你发一个书本习题分析或者leetcode题解博客,(ง๑ •̀_•́)ง 又立flag。
然后再说一下这个题目接替感想,确实这个题目花了不少时间,但是实际上这周来做的时候一下子就找到思路了,关键的在于如何将这个题目与动态规划扯上关系(就是记忆化数组max[from][to],[from,to]是符合条件的数字范围,然后就能在O(1)时间求出符合要求的最优数字,从而优化算法复杂度),可能确实是上周状态不好或者偏偏想不到这个问题的做法。以后作题可以考虑先把题目放一放,以后再来看或许会有意想不到的奇效。