题目是LeetCode第190场周赛的第四题,链接:1458. 两个子序列的最大点积。具体描述为:给你两个数组nums1
和nums2
。请你返回nums1
和nums2
中两个长度相同的非空子序列的最大点积。数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5]
是[1,2,3,4,5]
的一个子序列而[1,5,3]
不是。
示例1:
输入:nums1 = [2,1,-2,5], nums2 = [3,0,-6]
输出:18
解释:从 nums1 中得到子序列 [2,-2] ,从 nums2 中得到子序列 [3,-6] 。
它们的点积为 (2*3 + (-2)*(-6)) = 18 。
示例2:
输入:nums1 = [3,-2], nums2 = [2,-6,7]
输出:21
解释:从 nums1 中得到子序列 [3] ,从 nums2 中得到子序列 [7] 。
它们的点积为 (3*7) = 21 。
示例3:
输入:nums1 = [-1,-1], nums2 = [1,1]
输出:-1
解释:从 nums1 中得到子序列 [-1] ,从 nums2 中得到子序列 [1] 。
它们的点积为 -1 。
这种又是子序列又是求最大值的不出意料的应该就是动态规划的题目了,既然知道是动态规划,剩下的问题就是怎么求递推公式了。因为是两个序列,所以不出意外应该是个二维动态规划,那么就定义dp[i][j]
为nums1[0:i]
和nums2[0:j]
的最大点积,那最后的结果就是dp[m-1][n-1]
,其中m
、n
分别为nums1
和nums2
的长度。那么对于dp[i][j]
,要怎么通过之前的状态转移过来呢,可以根据nums1[i]
和nums2[j]
是否加入最大点积序列分情况处理:
nums1[i]
和nums2[j]
都加入最大点积序列,则dp[i][j]=max(nums1[i]*nums2[j], dp[i-1][j-1]+nums1[i]*nums2[j])
(注意不要漏掉nums1[i]*nums2[j]
这单独一项,因为dp[i-1][j-1]
有可能小于0)nums1[i]
不加入最大点积序列,则dp[i][j]=dp[i-1][j]
nums2[j]
不加入最大点积序列,则dp[i][j]=dp[i][j-1]
nums1[i]
和nums2[j]
都不加入最大点积序列,则dp[i][j]=dp[i-1][j-1]
时间复杂度为 O ( m n ) O(mn) O(mn),空间复杂度为 O ( m n ) O(mn) O(mn)。
JAVA版代码如下:
class Solution {
public int maxDotProduct(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int[][] dp = new int[m][n];
// dp[i][j]: nums1前i与nums2前j之间的最大点积
dp[0][0] = nums1[0] * nums2[0];
int max = nums1[0];
int min = nums1[0];
for (int i = 1; i < m; ++i) {
max = Math.max(nums1[i], max);
min = Math.min(nums1[i], min);
if (nums2[0] > 0) {
dp[i][0] = max * nums2[0];
}
else {
dp[i][0] = min * nums2[0];
}
}
max = nums2[0];
min = nums2[0];
for (int j = 1; j < n; ++j) {
max = Math.max(nums2[j], max);
min = Math.min(nums2[j], min);
if (nums1[0] > 0) {
dp[0][j] = max * nums1[0];
}
else {
dp[0][j] = min * nums1[0];
}
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
dp[i][j] = Math.max(Math.max(nums1[i] * nums2[j], Math.max(dp[i - 1][j - 1], dp[i - 1][j - 1] + nums1[i] * nums2[j])), Math.max(dp[i - 1][j], dp[i][j - 1]));
}
}
return dp[m - 1][n - 1];
}
}
提交结果如下:
然后还是固定套路,通过观察状态转移方程,可以看到状态最远只依赖于i-1
、j-1
,所以还是可以优化空间复杂度到
O
(
m
i
n
(
m
,
n
)
)
O(min(m,n))
O(min(m,n)),时间复杂度还是
O
(
m
n
)
O(mn)
O(mn)。
JAVA版代码如下:
class Solution {
public int maxDotProduct(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
if (m < n) {
return maxDotProduct(nums2, nums1);
}
int[] dp = new int[n];
// 第i次循环中的dp[j]: nums1前i与nums2前j之间的最大点积
dp[0] = nums1[0] * nums2[0];
int max = nums2[0];
int min = nums2[0];
for (int j = 1; j < n; ++j) {
max = Math.max(nums2[j], max);
min = Math.min(nums2[j], min);
if (nums1[0] > 0) {
dp[j] = max * nums1[0];
}
else {
dp[j] = min * nums1[0];
}
}
max = nums1[0];
min = nums1[0];
int dp_i_1_j_1;
for (int i = 1; i < m; ++i) {
max = Math.max(max, nums1[i]);
min = Math.min(min, nums1[i]);
dp_i_1_j_1 = dp[0];
if (nums2[0] > 0) {
dp[0] = max * nums2[0];
}
else {
dp[0] = min * nums2[0];
}
for (int j = 1; j < n; ++j) {
int tmp = dp[j];
dp[j] = Math.max(Math.max(nums1[i] * nums2[j], Math.max(dp_i_1_j_1, dp_i_1_j_1 + nums1[i] * nums2[j])), Math.max(tmp, dp[j - 1]));
dp_i_1_j_1 = tmp;
}
}
return dp[n - 1];
}
}
提交结果如下:
Python版代码如下:
class Solution:
def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
m = len(nums1)
n = len(nums2)
if m < n:
return self.maxDotProduct(nums2, nums1)
dp = [0] * n
dp[0] = nums1[0] * nums2[0]
maxN = minN = nums2[0]
for j in range(1, n):
maxN = max(maxN, nums2[j])
minN = min(minN, nums2[j])
dp[j] = maxN * nums1[0] if nums1[0] > 0 else minN * nums1[0]
maxN = minN = nums1[0]
for i in range(1, m):
dp_i_1_j_1 = dp[0]
maxN = max(maxN, nums1[i])
minN = min(minN, nums1[i])
dp[0] = maxN * nums2[0] if nums2[0] > 0 else minN * nums2[0]
for j in range(1, n):
tmp = dp[j]
dp[j] = max(nums1[i] * nums2[j], dp_i_1_j_1 + nums1[i] * nums2[j],
dp_i_1_j_1, dp[j - 1], tmp)
dp_i_1_j_1 = tmp
return dp[n - 1]
提交结果如下: