【每日一题Day64】LC1799N 次操作后的最大分数和 | 状态压缩dp 状态压缩+dfs+记忆化搜索
N 次操作后的最大分数和【LC1799】
You are given
nums
, an array of positive integers of size2 * n
. You must performn
operations on this array.In the
ith
operation (1-indexed), you will:
- Choose two elements,
x
andy
.- Receive a score of
i * gcd(x, y)
.- Remove
x
andy
fromnums
.Return the maximum score you can receive after performing
n
operations.The function
gcd(x, y)
is the greatest common divisor ofx
andy
.
给你
nums
,它是一个大小为2 * n
的正整数数组。你必须对这个数组执行n
次操作。在第
i
次操作时(操作编号从 1 开始),你需要:
- 选择两个元素
x
和y
。- 获得分数
i * gcd(x, y)
。- 将
x
和y
从nums
中删除。请你返回
n
次操作后你能获得的分数和最大为多少。函数
gcd(x, y)
是x
和y
的最大公约数。
虽然花了大半天 但是成就感满满
状态压缩dp
-
思路:
每个数字有两种状态,删除或者未删除,因此可以使用状态压缩记录未删除的数字情况state,便于使用位运算进行状态的遍历及转移;使用状态压缩dp找到将所以数字删除的最大分数
-
状态压缩:使用一个整数
s
来表示数组nums
中未删除的数字状态- 如果
s
的从右往左的第 i i i位为1说明原数组中的第 i i i位未删除 - 否则表示已删除
- 如果
-
状态dp:
-
dp数组含义:
d p [ i ] dp[i] dp[i]表示对于未删除的数字状态为 i i i时,往下进行操作能获得的最大分数
-
递推公式
对于每个状态
s
,如果该状态中二进制位为1的个数cnt
为偶数,那么可以进行交换操作:枚举s
中二进制为 1 1 1的位置,假设位置为 i i i和 j j j,则 i i i和 j j j两个位置的元素可以进行交换操作,此时可以获得的分数为 c n t / 2 ∗ g c d ( n u m s [ i ] , n u m s [ j ] ) cnt/2*gcd(nums[i],nums[j]) cnt/2∗gcd(nums[i],nums[j]),该状态由从状态s
中删除位置为 i i i和 j j j的状态转移得到,最后取最大值
d p [ s ] = m a x { d p [ s ⊕ 2 i ⊕ 2 j ] + c n t 2 ∗ g c d ( n u m s [ i ] , n u m s [ j ] ) } dp[s] = max \{dp[s\oplus 2^i \oplus 2^j] + \frac{cnt}{2}*gcd(nums[i],nums[j])\} dp[s]=max{dp[s⊕2i⊕2j]+2cnt∗gcd(nums[i],nums[j])} -
初始化
dp数组的长度为状态的个数,即 2 n 2^n 2n, n n n为数组的长度
dp[0] = 0;
-
遍历顺序
从小到大枚举所有状态
-
-
预处理:为了避免重复运算每一对数字的最大公约数,我们可以在「动态规划」前对数组中的每一对数字的最大公约数进行预处理操作,记录在数组
gcds
中。
-
-
实现
- 使用辗转相除法求得最大公约数,预处理得到每两个数的最大公约数,记录在
gcd
数组中 - 使用位运算判断是否是偶数、获得一个状态中1的个数、判断第 k k k位是否为1以及状态转移
class Solution { public int maxScore(int[] nums) { int n = nums.length; int[] dp = new int[1 << n]; int[][] gcds = new int[n][n]; // 预处理gcd for (int i = 0; i < n; i++){ for (int j = i + 1; j < n; j++){ gcds[i][j] = getGcd(nums[i], nums[j]); } } // 状态dp int ALL = (1 << n) - 1;// 全集 for (int s = 1; s <= ALL; s++){ int cnt = count1(s); // 1的个数为奇数 不合法 if ( (cnt & 1) == 1){ continue; } // 1的个数为偶数 for (int i = 0; i < n; i++){ if ( (s >> i & 1) == 1){ for (int j = i + 1; j < n; j++){ if ( (s >> j & 1) == 1){ dp[s] = Math.max(dp[s],dp[s ^ (1 << i) ^ (1 <<j)] + cnt / 2 * gcds[i][j] ); } } } } } return dp[ALL]; } public int count1 (int s){ int cnt = 0; for (int i = s; i > 0; i >>= 1){ cnt += i & 1; } return cnt; } public int getGcd (int x, int y){ return y != 0 ? getGcd(y, x % y) : x; } }
- 复杂度
- 时间复杂度: O ( 2 n ∗ n 2 + l o g C ∗ n 2 ) O(2^n*n^2+logC*n^2) O(2n∗n2+logC∗n2), n n n为数组的长度, C C C为数组中的最大元素,求最大公约数的时间复杂度为 O ( l o g C ) O(logC) O(logC),一共要求 n ∗ ( n − 1 ) n*(n-1) n∗(n−1)对,因此求取公约数总的时间复杂度为 O ( l o g C ∗ n 2 ) O(logC*n^2) O(logC∗n2),动态规划需要判断 2 n 2^n 2n个状态,每个状态需要使用双重循环寻找为1的位置,所需要的时间复杂度为 O ( 2 n ∗ n 2 ) O(2^n*n^2) O(2n∗n2)
- 空间复杂度:
O
(
2
n
+
n
2
)
O(2^n+n^2)
O(2n+n2),最大公约数数组和
dp
数组的空间开销
- 使用辗转相除法求得最大公约数,预处理得到每两个数的最大公约数,记录在
状态压缩+dfs+记忆化搜索
-
思路:使用记忆化搜索每个可能的状态,并记录至dp数组中,定义 d f s ( n u m s , s , c n t ) dfs(nums,s,cnt) dfs(nums,s,cnt) 表示数字的状态为
s
时,往下进行操作能获得的最大分数,其余参数的含义为:-
s
s
s表示表示数组
nums
中未删除的数字状态【整数】- 如果
s
的从右往左的第 i i i位为1说明原数组中的第 i i i位已删除【与方法1相反】 - 否则表示未删除
- 如果
- c n t cnt cnt表示当前进行的是第 c n t cnt cnt操作,与该次操作得分相关
那么 f ( n u m s , 0 , 1 ) f(nums,0,1) f(nums,0,1) 即为最终结果
-
s
s
s表示表示数组
-
实现
- 首先使用辗转相除法求得最大公约数,预处理得到每两个数的最大公约数,记录在
gcd
数组中 - 然后使用记忆化搜索得到最终结果
class Solution { int n, m; int[] dp; int[][] gcds; public int maxScore(int[] nums) { n = nums.length; m = n / 2; dp = new int[1 << n]; gcds = new int[n][n]; // 预处理gcd for (int i = 0; i < n; i++){ for (int j = i + 1; j < n; j++){ gcds[i][j] = getGcd(nums[i], nums[j]); } } return dfs(nums, 0, 1); } public int dfs(int[] nums, int s, int cnt){ if (cnt > m) return 0; if (dp[s] != 0) return dp[s]; int ans = 0; for (int i = 0; i < n; i++){ if (((s >> i) & 1) == 1) continue; for (int j = i + 1; j < n; j++){ if (((s >> j) & 1) == 1) continue; int next = s | (1 << i) | (1 << j); ans = Math.max(ans, cnt * gcds[i][j] + dfs(nums, next, cnt + 1)); } } dp[s] = ans; return ans; } public int getGcd(int x, int y){ return y != 0 ? getGcd(y, x % y) : x; } }
- 复杂度
- 时间复杂度: O ( 2 n ∗ n 2 + l o g C ∗ n 2 ) O(2^n*n^2+logC*n^2) O(2n∗n2+logC∗n2), n n n为数组的长度, C C C为数组中的最大元素,求最大公约数的时间复杂度为 O ( l o g C ) O(logC) O(logC),一共要求 n ∗ ( n − 1 ) n*(n-1) n∗(n−1)对,因此求取公约数总的时间复杂度为 O ( l o g C ∗ n 2 ) O(logC*n^2) O(logC∗n2),动态规划需要判断 2 n 2^n 2n个状态,每个状态需要使用双重循环寻找为1的位置,所需要的时间复杂度为 O ( 2 n ∗ n 2 ) O(2^n*n^2) O(2n∗n2)
- 空间复杂度:
O
(
2
n
+
n
2
)
O(2^n+n^2)
O(2n+n2),最大公约数数组、
dp
数组的空间开销和递归堆栈的开销
- 首先使用辗转相除法求得最大公约数,预处理得到每两个数的最大公约数,记录在
贪心
我defeat的贪心…不甘心
class Solution {
public int maxScore(int[] nums) {
int m = nums.length;
int n = m / 2;
boolean[] isUsed = new boolean[m];
Arrays.sort(nums);
// 计算1个个数 1与其他任何数字的gcd只能为1 因此最后处理1 挑其他数字挑剩下的即可
int[] gcds = new int[n];
int idx1 = -1;
while (nums[idx1 + 1] == 1){
idx1++;
gcds[idx1] = 1;
}
int i = idx1 + 1;
while (i < n){
// 第一个未使用过的数字
int j = idx1 + 1;
while(isUsed[j]){
j++;
}
// 找到其最大的gcd
int cur = 0;
int index = -1;
for (int k = j + 1; k < m; k++){
if (isUsed[k]) continue;
int gcd = getGcd(nums[j],nums[k]);
if (gcd > cur){
cur = gcd;
index = k;
}
}
isUsed[j] = true;
isUsed[index] = true;
// 存储gcd
gcds[i] = cur;
i++;
}
// 计算score
Arrays.sort(gcds);
int res = 0;
for (i = 0; i < n; i++){
res += (i + 1) * gcds[i];
}
return res;
}
public int getGcd (int x, int y){
return y != 0 ? getGcd(y, x % y) : x;
}
}