题面
暴力思路
先sort(任意两个数差值的最小值转化为相邻两个元素的差值min), 然后转化为全组合问题,列出所有可能的组合并进行求解即可。
class Solution { public: int sumOfPowers(vector<int>& nums, int k) { ios::sync_with_stdio(false); cin.tie(0), cout.tie(0); const int MOD = 1e9 + 7; int n = nums.size(); long long res = 0; sort(nums.begin(), nums.end()); vector<bool> st(n + 1, false); vector<int> path(k + 1, 0); function<void(int, int)> dfs = [&](int u, int stid){ path[u] = nums[stid]; if(u == k - 1){ int t = 1e9; for(int i = 0; i < k - 1; i ++) t = min(t, abs(path[i] - path[i + 1])); res = (long long) (res + t) % MOD; return; // 记得return, 否则会访问到k, k + 1超出idx索引范围 } for(int i = stid + 1; i < n; i ++){ st[i] = true; dfs(u + 1, i); st[i] = false; // 恢复现场 } }; for(int i = 0; i < nums.size(); i ++) // 可以剪枝一下 st[i] = 1, dfs(0, i), st[i] = 0; return res; } };
过了60%左右,会超时。
记忆化搜索+状压
①记忆化:比如... 1 3 4 和... 2 3 4,虽然选中的值是不同的,但当前的mi和上一个选中值一致,两者递归得到的能量和是一致的,故可以记忆化。
②状压:st状态值——题目给定n最大50,即11 0010,最多6位,其中i、j、k最大都是50,每个人占6位,剩下的留给mi,不同的i,j,mi,k组合会得到一个唯一的st值。
class Solution { public: int sumOfPowers(vector<int>& nums, int k) { int n = nums.size(), MOD = 1e9 + 7, INF = 0x3f3f3f3f; sort(nums.begin(), nums.end()); unordered_map<long long, int> dp; function<int(int, int, int, int)> dfs = [&](int idx, int mindiff, int pre, int k){ if(k == 0) return mindiff; // 成功构建len=k的子序列,返回当前的最小差值 if(idx == n || n - idx < k) return 0; // ①遍历完了整个数组仍然选不够k个元素,搜索作废 ②剩下元素一定不够用的,剪枝 long long st = (1LL * mindiff << 18) | (pre << 12) | (k << 6) | idx; // 状压 if(dp.count(st)) return dp[st]; int curdiff = mindiff; // 只要不是第一次选择元素, 就可以进行curdiff的维护 if(pre != n) curdiff = min(curdiff, nums[idx] - nums[pre]); int choose = dfs(idx + 1, curdiff, idx, k - 1); // 选择当前元素 int not_choose = dfs(idx + 1, mindiff, pre, k); // 不选择当前元素 return dp[st] = ((long long) choose + not_choose) % MOD; }; return dfs(0, INF, n, k); // dfs参数解释:从idx=0的元素开始枚举是否选择,当前由于没有差值则mindiff设置为+∞, // pre设置为n是为了方便表示当前是第一次选择数(就不计算与pre指向数字的diff)了 // k就是子序列的长度 } };
进行空间优化
参数一定必须上面 4个吗?观察递归方程,发现第一个参数的 i总是 +1表示当前元素,用来向下递归。方向太过单一,可以优化掉。
重新定义函数 dfs(minDiff, pre, k),表示已经拼接的序列末尾是第pre个元素,还需要选取 k个元素,当前的最小差值为 minDiff 时,能得到的最小能量和。
此时没有了当前元素,那么就用一个 for循环判断每一种当前的可能。
同时,递归的边界、剪枝、方程都需要修改。具体请见代码:
class Solution { public: int sumOfPowers(vector<int>& nums, int k) { int n = nums.size(), MOD = 1e9 + 7, INF = 0x3f3f3f3f; sort(nums.begin(), nums.end()); unordered_map<long long, int> dp; function<int(int, int, int)> dfs = [&](int mindiff, int pre, int k){ if(k == 0) return mindiff; // 成功构建len=k的子序列,返回当前的最小差值 if(pre < k) return 0; long long st = (1LL * mindiff << 12) | (pre << 6) | k ; // 状压 if(dp.count(st)) return dp[st]; int ans = 0; for(int j = 0; j < pre; j ++) ans = ((long long)ans + dfs(min(mindiff, nums[pre] - nums[j]), j, k - 1)) % MOD; return dp[st] = ans; }; int res = 0; for(int i = 1; i < n; i ++) res = ((long long)res + dfs(INF, i, k - 1)) % MOD; return res; } };
DP
class Solution { public: int sumOfPowers(vector<int>& nums, int k) { int n = nums.size(); const int MOD = 1e9 + 7; using hash = unordered_map<int, int>; // hash为了进一步压缩状态数目,也为了方便初始+∞的表示 sort(nums.begin(), nums.end()); // dp[i][len][power] = count 个数 // 表示 以索引i结尾的 长度为len的 能量为power的 所有子序列的 数量 vector<vector<hash>> dp(n, vector<hash>(k + 1)); int ans = 0; for(int i = 0; i < n; i ++){ dp[i][1][INT_MAX] = 1; for(int j = 0; j < i; j ++){ // 类似于最长上升子序列 int x = nums[i] - nums[j]; for(int len = 2; len <= k; len ++) for(auto& [diff, cnt]: dp[j][len - 1]) // 曲线救国:len - 1 dp[i][len][min(x, diff)] = (dp[i][len][min(x, diff)] + cnt) % MOD; } for (auto& [diff, cnt]: dp[i][k]) ans = (ans + (long long)diff * cnt % MOD) % MOD; // 根据f的定义:个数 * power即可 } return ans; } };