前言
从数组中找多个数字,其之和等于target,一般的思想就是dfs进行组合。
但耗时太多,则可以将dfs中重复计算的地方进行记录,空间换时间,即记忆搜索法。
但这种记忆搜索法只记录了一些重复计算的地方,还没将空间换时间发挥到极致。
此时可动态递推来完成求解,即动态规划。动态规划中的状态往往是层层递推的,所以在空间上可以做到进一步优化,即动态压缩。
一、目标和
二、dfs - > 状压
1、暴力dfs
// 暴力dfs
public int findTargetSumWays(int[] nums, int target) {
return dfs(nums,0,target);
}
private int dfs(int[] nums,int i,int target){
if(i == nums.length)
return target == 0 ? 1 : 0;
return dfs(nums,i + 1,target - nums[i]) + dfs(nums,i + 1,target + nums[i]);
}
2、状态记录
// 记忆数组
final static int N = 1000;
public int findTargetSumWays(int[] nums, int target) {
int[][] f = new int[nums.length][4 * N + 1];
return dfs(nums,0,target,f);
}
private int dfs(int[] nums,int i,int target,int[][] f){
if(i == nums.length)
return target == 0 ? 1 : 0;
if(f[i][target + 2 * N] != 0) return f[i][target + 2 * N];
f[i][target + 2 * N] = dfs(nums,i + 1,target - nums[i],f) + dfs(nums,i + 1,target + nums[i],f);
return f[i][target + 2 * N];
}
3、动态递推
// 动态规划。
// f[i][j]:表示前i个数组成和为target的个数。
final static int N = 1000;
public int findTargetSumWays(int[] nums, int target) {
int[][] f = new int[nums.length + 1][4 * N + 1];
f[0][2 * N] = 1;
for(int i = 1;i <= nums.length;i++){
for(int j = 0;j <= 4 * N;j++){
if(j >= nums[i - 1]) f[i][j] = f[i - 1][j - nums[i - 1]];
if(j + nums[i - 1] <= 4 * N) f[i][j] += f[i - 1][j + nums[i - 1]];
}
}
return f[nums.length][2 * N + target];
}
4、状态压缩
// 状态压缩,由于每次状态都和上一层有关,且和前面的未知位置有关,所以采用一维数组,
// 由于和前后状态都有关,所有需两个一维数组。
final static int N = 1000;
public int findTargetSumWays(int[] nums, int target) {
int[][] f = new int[2][4 * N + 1];
int idx = 0;
f[idx][2 * N] = 1;
for(int i = 1;i <= nums.length;i++){
for(int j = 0;j <= 4 * N;j++){
f[(idx + 1) & 1][j] = 0;
if(j >= nums[i - 1]) f[(idx + 1) & 1][j] = f[idx][j - nums[i - 1]];
if(j + nums[i - 1] <= 4 * N) f[(idx + 1) & 1][j] += f[idx][j + nums[i - 1]];
}
idx = (++idx) & 1;
}
return f[idx][2 * N + target];
}
总结
1)从最简单的dfs暴力逻辑;到利用空间记录重复计算,即记忆搜索;再到极致的空间换时间,即动态规划;最后到空间优化,即状态压缩。形成一个完整的逻辑链,一个完整的优化闭环。
2)做好完整逻辑闭环,知其中每个细节点的因果逻辑,才能充分理解它,做到融合贯通,举一反三。
参考文献
[1] LeetCode 目标和