动态规划之线性DP-安全序列

问题描述

小蓝是工厂里的安全工程师,他负责安放工厂里的危险品。

工厂是一条直线,直线上有 n 个空位,小蓝需要将若干个油桶放置在 n 个空位上,每 2 个油桶中间至少需要 k 个空位隔开,现在小蓝想知道有多少种放置油桶的方案,你可以编写一个程序帮助他吗?

由于这个结果很大,你的输出结果需要对  取模。

输入格式

第一行包含两个正整数n,k,分别表示 n 个空位与 k 个隔开的空位。

输出格式

输出共 1行,包含 1 个整数,表示放置的方案数对  取模。

样例输入

4 2

样例输出

6

说明

用 0代表不放,1 代表放,6 种情况分别为:

000010000100001000011001

评测数据规模

对于所有评测数据,

思路:

当放一个油桶时,每一个空位都有一个可能

当放两个油桶时,则前 k 个空位不能放油桶,最多在第 i-k-1 个位置才能放油桶,方案数等于 dp[i-k-1]

解题代码:

#include <iostream>
using namespace std;
typedef long long ll;
const ll N=1e6+10,p=1e9+7;

ll dp[N],prefix[N];
int main()
{
  int n,k;cin>>n>>k;
  dp[0]=prefix[0]=1;
  for(int i=1;i<=n;i++)
  {
    if(i-k-1<1)dp[i]=1;
    else dp[i]=prefix[i-k-1];
    prefix[i]=(prefix[i-1]+dp[i])%p;
  }
  cout<<prefix[n]<<'\n';
  return 0;
}

1、动态规划定义

  • dp[i]:表示前 i 个空位的放置方案数。

  • prefix[i]:表示前 i 个空位的前缀和,即 prefix[i] = dp[0] + dp[1] + ... + dp[i]

 2、动态规划转移方程

  • 对于第 i 个空位:

    • 如果选择不放油桶,则方案数等于 dp[i-1]

    • 如果选择放油桶,则前 k 个空位不能放油桶,方案数等于 dp[i-k-1]

  • 因此,动态规划转移方程为:

    dp[i] = dp[i-1] + dp[i-k-1]

    其中:

    • dp[i-1]:不放油桶的方案数。

    • dp[i-k-1]:放油桶的方案数。

3、前缀和的作用

  • 前缀和 prefix[i] 用于快速计算 dp[i]

  • 由于 dp[i] 依赖于 dp[i-1] 和 dp[i-k-1],而 dp[i-1] 可以通过 prefix[i-1] - prefix[i-2] 计算,dp[i-k-1] 可以直接通过 prefix[i-k-1] 计算。

  • 使用前缀和可以将动态规划的转移优化为 O(1) 的时间复杂度。

4、示例分析

假设输入:

n = 4, k = 2
(1) 初始化
  • dp[0] = prefix[0] = 1

(2) 动态规划转移
  • i = 1

    • i - k - 1 = 1 - 2 - 1 = -2 < 1,所以 dp[1] = 1

    • prefix[1] = (prefix[0] + dp[1]) % p = (1 + 1) % p = 2

  • i = 2

    • i - k - 1 = 2 - 2 - 1 = -1 < 1,所以 dp[2] = 1

    • prefix[2] = (prefix[1] + dp[2]) % p = (2 + 1) % p = 3

  • i = 3

    • i - k - 1 = 3 - 2 - 1 = 0 < 1,所以 dp[3] = 1

    • prefix[3] = (prefix[2] + dp[3]) % p = (3 + 1) % p = 4

  • i = 4

    • i - k - 1 = 4 - 2 - 1 = 1 >= 1,所以 dp[4] = prefix[1] = 2

    • prefix[4] = (prefix[3] + dp[4]) % p = (4 + 2) % p = 6

(3) 结果
  • prefix[4] = 6,表示有 6 种放置方案。

5、为什么返回 prefix[n]

  • prefix[n] 表示前 n 个空位的总方案数。

  • 因为 dp[i] 只表示前 i 个空位的方案数,而 prefix[i] 是 dp[0] 到 dp[i] 的累加和。

  • 最终答案需要包括所有可能的方案数,因此返回 prefix[n]

6、动态规划的过程总结

  1. 定义状态dp[i] 表示前 i 个空位的放置方案数。

  2. 转移方程dp[i] = dp[i-1] + dp[i-k-1]

  3. 前缀和优化:使用 prefix[i] 快速计算 dp[i]

  4. 初始化dp[0] = prefix[0] = 1

  5. 结果:返回 prefix[n],表示前 n 个空位的总方案数。

<think>嗯,用户想要了解Java中动态规划线性DP,我需要详细讲解。首先,得确定用户对动态规划的基本概念是否熟悉。可能他们知道一些,但作为讲解,还是应该从基础开始,再逐步深入。 线性DP,应该指的是问题模型是线性的,比如一维数组或者序列上的问题。常见的例子有斐波那契数列、最长递增子序列(LIS)、最长公共子序列(LCS)这些。这些例子比较典型,用户可能听说过,可以用来举例说明。 接下来,我需要明确线性DP的核心步骤:定义状态、状态转移方程、初始化和计算顺序。这部分是关键,得详细解释每个步骤的作用。比如状态定义,要说明为什么用一维数组,或者二维但有一个维度是线性变化的。 然后,可能需要举几个具体的例子。比如斐波那契数列作为入门例子,虽然简单,但能展示状态转移。然后是LIS问题,这里可以用O(n&sup2;)的解法,说明状态转移的过程。可能还要提到更优化的O(n log n)解法,但用户可能主要关注线性DP的基础,所以先讲基本的。 再考虑LCS问题,虽然是二维DP,但可以归为线性结构,因为两个序列都是线性遍历的。需要解释状态的定义和转移方程如何推导出来。 在Java代码实现部分,要给出具体的例子,比如斐波那契的迭代版本,避免递归带来的重复计算。LIS问题则需要双重循环,展示如何填充dp数组。代码要简洁,注释清晰,方便用户理解。 可能还需要总结线性DP的特点,比如状态定义通常与问题规模相关,状态转移依赖于之前的状态等。此外,注意事项部分应该包括空间优化,比如滚动数组,或者状态压缩的技巧,这对实际应用有帮助。 用户可能有疑问,如何识别问题是否适用线性DP,这里需要给出一些判断方法,比如问题是否涉及序列或数组,子问题是否可以线性分解等。同时,错误排查方面,比如初始化是否正确,边界条件是否处理妥当,这些也是常见问题。 最后,确保整个结构清晰,分点讲解,由浅入深,让用户能够逐步理解和应用。需要检查是否覆盖了线性DP的主要知识点,并且示例代码正确无误,符合Java语法规范。</think>好的!我将分步骤为您详细讲解Java中动态规划线性DP实现,结合核心原理、经典模型和代码示例。 --- ### **一、线性DP核心理解** **定义**:在一维或伪一维(如双序列)结构上,按线性顺序推导状态的动态规划。 **特点**:状态转移方程仅依赖前序有限个状态,时间复杂度通常为 $O(n)$ 或 $O(n^2)$。 --- ### **二、实现四步骤** #### **1. 定义状态** - 使用一维数组 `dp[i]`,表示以第 `i` 个元素结尾的子问题最优解 - 例:最长递增子序列中,`dp[i]` 表示以 `nums[i]` 结尾的LIS长度 #### **2. 状态转移方程** - 关键:找到 `dp[i]` 与 `dp[j] (j < i)` 的关系 - 例(LIS): $$dp[i] = \max_{0 \le j < i}(dp[j] + 1) \quad \text{当} \ nums[j] < nums[i]$$ #### **3. 初始化** - 初始状态通常为边界值,如 `dp[0] = 1` #### **4. 计算顺序** -线性顺序(如从左到右)填充 `dp` 数组 --- ### **三、经典模型与Java代码** #### **模型1:斐波那契数列** ```java int fib(int n) { if (n <= 1) return n; int[] dp = new int[n+1]; dp[0] = 0; dp[1] = 1; for (int i = 2; i <= n; i++) { dp[i] = dp[i-1] + dp[i-2]; // 状态转移 } return dp[n]; } ``` #### **模型2:最长递增子序列(LIS)** ```java int lengthOfLIS(int[] nums) { int[] dp = new int[nums.length]; Arrays.fill(dp, 1); int maxLen = 1; for (int i = 1; i < nums.length; i++) { for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } maxLen = Math.max(maxLen, dp[i]); } return maxLen; } ``` #### **模型3:最长公共子序列(LCS)** ```java int longestCommonSubsequence(String text1, String text2) { int m = text1.length(), n = text2.length(); int[][] dp = new int[m+1][n+1]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (text1.charAt(i-1) == text2.charAt(j-1)) { dp[i][j] = dp[i-1][j-1] + 1; } else { dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); } } } return dp[m][n]; } ``` --- ### **四、优化技巧** 1. **空间压缩**:若状态仅依赖前几个值,可用滚动数组 (例:斐波那契数列只需保存前两个状态) 2. **贪心+二分优化LIS**:时间复杂度降至 $O(n \log n)$ --- ### **五、常见问题排查** 1. **初始化错误**:检查 `dp[0]` 是否符合场景 2. **边界越界**:注意数组索引与字符串位置的偏移 3. **转移方程遗漏条件**:如LCS中字符不相等的情况 --- ### **六、适用场景判断** 当问题满足以下特征时,优先考虑线性DP: 1. 输入为序列或可线性展开的结构 2. 问题的最优解可分解为子问题的最优解 3. 子问题之间存在明显的递推关系 通过以上步骤拆解和代码实践,可以系统掌握线性DP的核心设计方法!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值