实际问题的算法抽象——动态规划中的01背包问题

本文解析了LeetCode上的两道题目,LeetCode494和416,通过数学抽象和动态规划的方法,给出了清晰的问题解决思路,并提供了具体的代码实现。

  LeetCode的许多题目都不会明确告诉你用哪一类算法进行求解,甚至可能都无须用复杂算法。然而能明确用哪一类算法解决问题,将对问题的解决提供事半功倍的效果。当然有些题目即使告诉你用哪一类算法,然而如何将该问题抽象成一个数学算法,也是需要锻炼的。这里结合LeetCode 494和416给大家做一点分析。

数学抽象——LeetCode 494

You are given a list of non-negative integers, a1, a2, …, an, and a target, S. Now you have 2 symbols + and -. For each integer, you should choose one from + and - as its new symbol.

Find out how many ways to assign symbols to make sum of integers equal to target S.

Example 1:
Input: nums is [1, 1, 1, 1, 1], S is 3.
Output: 5
Explanation:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

There are 5 ways to assign symbols to make the sum of nums be target 3.
Note:
The length of the given array is positive and will not exceed 20.
The sum of elements in the given array will not exceed 1000.
Your output answer is guaranteed to be fitted in a 32-bit integer.

  摆脱跳跃性大脑思维,按照电脑的“笨”办法,我初步想法如下:
  1. 先看所给目标S是否在int数组元素的正和和负和范围内,如果不是的话,不可行。如果在范围内,进行下一步;
  2. 每一个数都有正负两种可能,对每一个数都进行两种判断,总共有2的n次方中可能,看看其中有几种可以即刻。

  明显上述是一个理论可行,实际时间复杂度太高的方法。因此需要进一步的优化。这里需要对问题进行数学加工。
  引用LeetCode的Discuss中的一个经典答案。我们把加上符号后的序列分为正子序列P和负子序列N。则可以得到以下公式。

         sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
            2 * sum(P) = target + sum(nums)

  是不是瞬间明朗很多了呢。也就是说我们在保证序列和在目标范围内(sum(nums)<|target|)且序列和与目标的和是偶数((target+sum(nums))%2 == 0)的前提下,只需要找到一个正子序列P,使得 sum(P) = (target + sum(nums)) / 2 即可。
  这是第一步,第二步则是找到这个正子序列P。
  给定一个序列nums,给定一个目标和sum(P),求是否能从nums中取出一定的元素,使其和为sum(P)。这个问题是不是很眼熟呢?没错,就是动态规划中的01背包问题。问题分析至此也就十分明朗了。
  代码如下:

public int findTargetSumWays(int[] nums, int s) {
    int sum = 0;
    for (int n : nums)
        sum += n;
    return sum < s || (s + sum) % 2 > 0 ? 0 : subsetSum(nums, (s + sum) >>> 1); 
}   

//01背包问题
public int subsetSum(int[] nums, int s) {
    int[] dp = new int[s + 1]; 
    dp[0] = 1;
    for (int n : nums)
        for (int i = s; i >= n; i--)
            dp[i] += dp[i - n]; 
    return dp[s];
} 

  暂时先让我们看一下以下关于01背包问题的阐述与思路。

动态规划之01背包问题——LeetCode 416

Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Note:
Each of the array element will not exceed 100.
The array size will not exceed 200.

  这个问题还是比较明确的,给定一个正整数序列nums,给定一个目标和sum(nums)/2,求是否能从nums中取出一定的元素,使其和为sum(nums)/2。
  对于01背包问题,引用一个我感觉还不错的讲解 http://blog.youkuaiyun.com/xiaoquantouer/article/details/53454346
  最关键的就是记住01背包问题的状态转移公式:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]},背景如下:有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i],f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。
  模板伪代码:
  伪代码:
  for i = 1 … N
    for v = V…0
      f[v] = max( f[v], f[v-c[i]] + w[i])
  根据这个公式以及上述网页的阐述,这道题的解法如下链接所示 LeetCode 416
  优化时间复杂度后,如下:

public boolean canPartition(int[] nums) {
    int sum = 0;

    for (int num : nums) {
        sum += num;
    }

    if ((sum & 1) == 1) {
        return false;
    }
    sum /= 2;

    int n = nums.length;
    boolean[] dp = new boolean[sum+1];
    Arrays.fill(dp, false);
    dp[0] = true;

    for (int num : nums) {
        for (int i = sum; i > 0; i--) {
            if (i >= num) {
                dp[i] = dp[i] || dp[i-num];
            }
        }
    }

    return dp[sum];
}

  这里的关键部分是如下这段:

    for (int num : nums) {
         for (int i = sum; i > 0; i--) {
             if (i >= num) {
                 dp[i] = dp[i] || dp[i-num];
             }
         }
     }

     return dp[sum];

  这里等号左边的dp[i]代表当容量为i时是否能满足要求(等同于01背包问题中的获得最大价值f[v])。等号右边的dp[i]代表将num这一项放入背包时是否能满足要求,dp[i-num]代表将num这一项放入背包时是否能满足要求。对于每一项物品num,都进行判断,判断其在背包剩余容量从sum到num时的要求满足情况。满足的情况保存下来,留给下一项物品num,作为判断的基础,即在下一项num判断时,有一些背包容量(可能容量还未达到sum,但这是为最终的背包容量sum做铺垫)已经满足要求了。

01背包问题 根据具体情境进行调整——LeetCode 494【续】

  刚才这一题代码的01背包问题部分如下:

public int findTargetSumWays(int[] nums, int s) {
    int sum = 0;
    for (int n : nums)
        sum += n;
    return sum < s || (s + sum) % 2 > 0 ? 0 : subsetSum(nums, (s + sum) >>> 1); 
}   

//01背包问题
public int subsetSum(int[] nums, int s) {
    int[] dp = new int[s + 1]; 
    dp[0] = 1;
    for (int n : nums)
        for (int i = s; i >= n; i--)
            dp[i] += dp[i - n]; 
    return dp[s];
} 

  这个与LeetCode 416有点区别。416初始化时是一个个boolean值(是否满足要求),这里初始化时是整形值(是否塞满背包)。但套用的公式都一样,记住公式即可。
  值得注意的是这两道题其实都只有不同物品消耗的背包容量,而没有他们提供的价值。

内容概要:本文介绍了一个关于超声谐波成像中幅度调制聚焦超声所引起全场位移和应变的分析模型,并提供了基于Matlab的代码实现。该模型旨在精确模拟和分析在超声谐波成像过程中,由于幅度调制聚焦超声作用于生物组织时产生的力学效应,包括全场的位移与应变分布,从而为医学成像和治疗提供理论支持和技术超声谐波成像中幅度调制聚焦超声引起的全场位移和应变的分析模型(Matlab代码实现)手段。文中详细阐述了模型构建的物理基础、数学推导过程以及Matlab仿真流程,具有较强的理论深度与工程应用价值。; 适合人群:具备一定声学、生物医学工程或力学背景,熟悉Matlab编程,从事医学成像、超声技术或相关领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①用于超声弹性成像中的力学建模与仿真分析;②支持高强度聚焦超声(HIFU)治疗中的组织响应预测;③作为教学案例帮助理解超声与组织相互作用的物理机制;④为相关科研项目提供可复用的Matlab代码框架。; 阅读建议:建议读者结合超声物理和连续介质力学基础知识进行学习,重点关注模型假设、偏微分方程的数值求解方法及Matlab实现细节,建议动手运行并修改代码以加深理解,同时可拓展应用于其他超声成像或治疗场景的仿真研究。
### 关于PAT Basic Level Practice的测试点及题目解析 #### 题目难度分级 PAT(Programming Ability Test)是由浙江大学举办的计算机程序设计能力考试,分为不同级别。其中乙级即Basic Level主要面向初学者,考察基本编程技能[^1]。 #### 测试点特点 对于PAT Basic Level中的某些特定题目而言,其测试点设置较为严格。例如,在处理字符串匹配类问题时,需要注意算法逻辑中何时应当终止循环以防止不必要的重复计算;而在涉及数值运算的问题里,则可能因为边界条件而增加复杂度[^3]。 #### 编程语言的选择影响 值得注意的是,尽管大部分简单题目可以作为学习某种新语言的良好实践材料,但在实际操作过程中可能会遇到由于所选语言特性而导致难以通过全部测试点的情况。比如Java在面对部分效率敏感型试题时表现不佳,这可能是由于该语言本身的执行速度相对较慢以及内存管理方式等因素造成的。因此有时不得不转而采用其他更适合解决此类问题的语言版本来完成解答[^2]。 ```cpp #include<bits/stdc++.h> using namespace std; int a[100000]; int c=1; void getPrime(){ int flag=0; for(int i=2;i<105000;i++){ flag=1; for(int j=2;j<=sqrt(i);j++){ if(i%j==0){ flag=0; break; } } if(flag==1) a[c++]=i; } } int main(){ int m,n,i,t=1; scanf("%d %d",&m,&n); getPrime(); for(i=m;i<=n;i++){ if(t%10==1){ printf("%d",a[i]); t++; }else{ printf(" %d",a[i]); t++; } if((t-1)%10==0) printf("\n"); } return 0; } ``` 上述C++代码展示了如何实现一个简单的质数打印功能,并且针对输出格式进行了特殊处理以满足特定要求。这段代码很好地体现了编写高效解决方案的重要性,尤其是在应对像PAT这样的在线评测系统时[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值