Medium
题目描述
给定一维平面上的 n 个点,其中第 i 个点(从 0 到 n-1)位于 x = i 处,找到我们可以恰好画出 k 个不重叠线段的方法数,使得每个线段覆盖两个或更多点。每个线段的端点必须具有整数坐标。k 个线段不必覆盖所有 n 个点,并且它们可以共享端点。
返回我们可以画出 k 个不重叠线段的方法数。由于这个数字可能很大,请将其模 10^9 + 7 返回。
示例 1:
输入:n = 4, k = 2
输出:5
解释:两条线段分别用红色和蓝色显示。
上图显示了5种不同的方式:{(0,2),(2,3)}, {(0,1),(1,3)}, {(0,1),(2,3)}, {(1,2),(2,3)}, {(0,1),(1,2)}。
示例 2:
输入:n = 3, k = 1
输出:3
解释:3种方式是 {(0,1)}, {(0,2)}, {(1,2)}。
示例 3:
输入:n = 30, k = 7
输出:796297179
解释:画7条线段的可能方式总数为3796297200。取模 10^9 + 7 得到 796297179。
约束条件:
- 2 <= n <= 1000
- 1 <= k <= n-1
解题思路
这道题可以用动态规划来解决。核心思想是考虑每个点的状态:是否在某个线段的内部,以及已经放置了多少个线段。
状态定义:
dp[i][j][flag]表示考虑前 i 个点,已放置 j 个线段,flag 表示当前是否处于线段内部的方案数- flag = 0:当前不在任何线段内部
- flag = 1:当前在某个线段内部(已有起点但未确定终点)
状态转移: 对于每个位置 i,我们有以下选择:
如果 flag = 0(当前不在线段内):
- 不做任何操作:
dp[i+1][j][0] += dp[i][j][0] - 在位置 i 开始新线段:
dp[i+1][j][1] += dp[i][j][0]
- 不做任何操作:
如果 flag = 1(当前在线段内):
- 继续延长当前线段:
dp[i+1][j][1] += dp[i][j][1] - 在位置 i 结束当前线段:
dp[i+1][j+1][0] += dp[i][j][1] - 在位置 i 结束当前线段并开始新线段:
dp[i+1][j+1][1] += dp[i][j][1]
- 继续延长当前线段:
这种方法的时间复杂度为 O(n²k),但可以优化为 O(nk)。实际上,这个问题还有组合数学的解法,答案为 C(n+k-1, 2k)。
代码实现
class Solution {
public:
int numberOfSets(int n, int k) {
const int MOD = 1e9 + 7;
// dp[i][j][flag] - 考虑前i个点,放置j个线段,flag表示是否在线段内
// flag=0: 不在线段内, flag=1: 在线段内
vector<vector<vector<long long>>> dp(n + 1, vector<vector<long long>>(k + 1, vector<long long>(2, 0)));
dp[0][0][0] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= k; j++) {
// 当前不在线段内
dp[i + 1][j][0] = (dp[i + 1][j][0] + dp[i][j][0]) % MOD; // 不操作
dp[i + 1][j][1] = (dp[i + 1][j][1] + dp[i][j][0]) % MOD; // 开始新线段
// 当前在线段内
dp[i + 1][j][1] = (dp[i + 1][j][1] + dp[i][j][1]) % MOD; // 继续线段
if (j + 1 <= k) {
dp[i + 1][j + 1][0] = (dp[i + 1][j + 1][0] + dp[i][j][1]) % MOD; // 结束线段
dp[i + 1][j + 1][1] = (dp[i + 1][j + 1][1] + dp[i][j][1]) % MOD; // 结束并开始新线段
}
}
}
return dp[n][k][0];
}
};
class Solution:
def numberOfSets(self, n: int, k: int) -> int:
MOD = 10**9 + 7
# dp[i][j][flag] - 考虑前i个点,放置j个线段,flag表示是否在线段内
# flag=0: 不在线段内, flag=1: 在线段内
dp = [[[0] * 2 for _ in range(k + 1)] for _ in range(n + 1)]
dp[0][0][0] = 1
for i in range(n):
for j in range(k + 1):
# 当前不在线段内
dp[i + 1][j][0] = (dp[i + 1][j][0] + dp[i][j][0]) % MOD # 不操作
dp[i + 1][j][1] = (dp[i + 1][j][1] + dp[i][j][0]) % MOD # 开始新线段
# 当前在线段内
dp[i + 1][j][1] = (dp[i + 1][j][1] + dp[i][j][1]) % MOD # 继续线段
if j + 1 <= k:
dp[i + 1][j + 1][0] = (dp[i + 1][j + 1][0] + dp[i][j][1]) % MOD # 结束线段
dp[i + 1][j + 1][1] = (dp[i + 1][j + 1][1] + dp[i][j][1]) % MOD # 结束并开始新线段
return dp[n][k][0]
public class Solution {
public int NumberOfSets(int n, int k) {
const int MOD = 1000000007;
// dp[i][j][flag] - 考虑前i个点,放置j个线段,flag表示是否在线段内
// flag=0: 不在线段内, flag=1: 在线段内
long[,,] dp = new long[n + 1, k + 1, 2];
dp[0, 0, 0] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= k; j++) {
// 当前不在线段内
dp[i + 1, j, 0] = (dp[i + 1, j, 0] + dp[i, j, 0]) % MOD; // 不操作
dp[i + 1, j, 1] = (dp[i + 1, j, 1] + dp[i, j, 0]) % MOD; // 开始新线段
// 当前在线段内
dp[i + 1, j, 1] = (dp[i + 1, j, 1] + dp[i, j, 1]) % MOD; // 继续线段
if (j + 1 <= k) {
dp[i + 1, j + 1, 0] = (dp[i + 1, j + 1, 0] + dp[i, j, 1]) % MOD; // 结束线段
dp[i + 1, j + 1, 1] = (dp[i + 1, j + 1, 1] + dp[i, j, 1]) % MOD; // 结束并开始新线段
}
}
}
return (int)dp[n, k, 0];
}
}
var numberOfSets = function(n, k) {
const MOD = 1000000007;
// dp[i][j][flag] - 考虑前i个点,放置j个线段,flag表示是否在线段内
// flag=0: 不在线段内, flag=1: 在线段内
const dp = Array(n + 1).fill().map(() =>
Array(k + 1).fill().map(() => Array(2).fill(0))
);
dp[0][0][0] = 1;
for (let i = 0; i < n; i++) {
for (let j = 0; j <= k; j++) {
// 当前不在线段内
dp[i + 1][j][0] = (dp[i + 1][j][0] + dp[i][j][0]) % MOD; // 不操作
dp[i + 1][j][1] = (dp[i + 1][j][1] + dp[i][j][0]) % MOD; // 开始新线段
// 当前在线段内
dp[i + 1][j][1] = (dp[i + 1][j][1] + dp[i][j][1]) % MOD; // 继续线段
if (j + 1 <= k) {
dp[i + 1][j + 1][0] = (dp[i + 1][j + 1][0] + dp[i][j][1]) % MOD; // 结束线段
dp[i + 1][j + 1][1] = (dp[i + 1][j + 1][1] + dp[i][j][1]) % MOD; // 结束并开始新线段
}
}
}
return dp[n][k][0];
};
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 动态规划 | O(n × k) | O(n × k) |