1 条题解

  • 1
    @ 2025-10-9 8:55:09

    深入理解动态规划解决子集和问题

    让我用更直观的方式解释为什么通过做差更新DP表可以得到结果。我会用具体的例子和步骤来详细说明。

    基本概念

    想象一下,我们有一个背包,我们需要判断是否能恰好装进总重量为sum的物品。每个物品的重量就是数组中的一个数字。

    DP表dp[j]表示:​是否可以通过选择前i个物品中的一些,使得总重量恰好为j。

    详细步骤解析

    让我们用一个具体的例子来说明:nums = [1, 2, 3], sum = 6

    初始化阶段

    vector<bool> dp(sum + 1, false);
    dp[0] = true;  // 空集的和为0
    

    初始状态:

    dp = [true, false, false, false, false, false, false]

    这表示:不使用任何数字,我们可以得到和为0

    处理第一个数字:1

    for (int j = sum; j >= num; j--) 
    {
        if (dp[j - num]) 
        {
            dp[j] = true;
        }
    }
    

    处理过程:

    j=6: 检查dp[6-1]=dp[5]=false → 不更新

    j=5: 检查dp[5-1]=dp[4]=false → 不更新

    j=4: 检查dp[4-1]=dp[3]=false → 不更新

    j=3: 检查dp[3-1]=dp[2]=false → 不更新

    j=2: 检查dp[2-1]=dp[1]=false → 不更新

    j=1: 检查dp[1-1]=dp[0]=true → 设置dp[1]=true

    更新后:

    dp = [true, true, false, false, false, false, false]

    这表示:使用数字1,我们可以得到和为1

    处理第二个数字:2

    for (int j = sum; j >= num; j--) 
    {
        if (dp[j - num]) 
        {
            dp[j] = true;
        }
    }
    

    处理过程:

    j=6: 检查dp[6-2]=dp[4]=false → 不更新

    j=5: 检查dp[5-2]=dp[3]=false → 不更新

    j=4: 检查dp[4-2]=dp[2]=false → 不更新

    j=3: 检查dp[3-2]=dp[1]=true → 设置dp[3]=true

    j=2: 检查dp[2-2]=dp[0]=true → 设置dp[2]=true

    更新后:

    dp = [true, true, true, true, false, false, false]

    这表示:

    使用数字2,我们可以得到和为2

    使用数字1和2,我们可以得到和为3

    处理第三个数字:3

    for (int j = sum; j >= num; j--) 
    {
        if (dp[j - num]) 
        {
            dp[j] = true;
        }
    }
    

    处理过程:

    j=6: 检查dp[6-3]=dp[3]=true → 设置dp[6]=true

    j=5: 检查dp[5-3]=dp[2]=true → 设置dp[5]=true

    j=4: 检查dp[4-3]=dp[1]=true → 设置dp[4]=true

    j=3: 检查dp[3-3]=dp[0]=true → 设置dp[3]=true

    更新后:

    dp = [true, true, true, true, true, true, true]

    这表示:我们可以得到所有从0到6的和

    为什么做差更新有效?

    关键理解:​dp[j - num]表示的是"不使用当前数字num时,能否得到和j-num"​​

    当我们检查dp[j - num]时:

    如果它为true,意味着存在一个子集,其和为j - num

    那么,如果我们选择当前数字num,就可以得到和为(j - num) + num = j

    因此,我们可以设置dp[j] = true

    为什么需要逆向遍历?

    逆向遍历是为了避免重复使用同一个数字。

    假设我们正向遍历(从小到大):

    处理数字1:设置dp[1]=true

    处理数字2:当j=3时,检查dp[3-2]=dp[1]=true → 设置dp[3]=true

    处理数字3:当j=6时,检查dp[6-3]=dp[3]=true → 设置dp[6]=true

    看起来也能得到正确结果,但这是因为我们的数字都是正数且没有重复使用的问题。

    但如果数字可以重复使用,或者我们需要考虑组合情况,正向遍历就会出错。

    ​逆向遍历保证了我们在更新dp[j]时,dp[j - num]还没有被当前数字更新过,也就是说,dp[j - num]表示的是不考虑当前数字时的状态。

    完整代码再实现

    #include <iostream>
    #include <vector>
    using namespace std;
    int main()
    {
    	int n, sum;
    	cin >> n >> sum; 
    	vector<int> nums(n);
    	for (int i = 0; i < n; i++)
    	{
    		cin >> nums[i];
    	}
    	
    	if (sum == 0) // 目标和为0不可能被实现,输出NO 
    	{
    		cout << "NO" << endl;
    		return 0;
    	}
    	
    	int total = 0;
    	
    	for (int num : nums)
    	{
    		total += num;
    	}
    	
    	if (sum > total) // 目标和大于数字总和直接输出NO 
    	{
    		cout << "NO" << endl;
    		return 0;
    	}
    	else if (sum == total) // 数字总和与目标和相等则直接输出YES 
    	{
    		cout << "YES" << endl;
    		return 0;
    	}
    	
    	vector<bool> DP(sum + 1, false);
    	DP[0] = true;
    	
    	// DP列表表示能凑出来的和,若DP[i] = true这说明i可以被nums中的某些数字凑出来 
    	
    	for (int num : nums)
    	{
    		for (int j = sum; j >= num; j--)
    		{
    			// j作为目标总和时,与当前num的差可以被凑出来(之前某些数的和)即 j = num + 之前某些数字的和,
    			// 则说明 j 可以被凑出来,记录在DP表中 
    			if (DP[i - num])
    			{
    				DP[i] = true;
    			}
    		}
    	}
    	
    	// 查询DP表,若sum被标记了可以被凑出来(标记为true),则输出YES 
    	if (DP[sum])
    	{
    		cout << "YES" << endl;
    	}
    	else
    	{
    		cout << "NO" << endl;
    	}
    	return 0;
    }
    
    
    • 1

    信息

    ID
    205
    时间
    10ms
    内存
    256MiB
    难度
    9
    标签
    递交数
    11
    已通过
    5
    上传者