蓝桥杯C语言中的动态规划问题研究
摘要
动态规划是解决多阶段决策问题的一种高效算法,在蓝桥杯C语言竞赛中应用广泛。本文系统地介绍了动态规划的基本概念、原理及解题步骤,并通过多个经典例题,结合表格和代码实例,展示了动态规划在不同问题中的应用方法。本文旨在帮助参赛选手更好地理解和掌握动态规划,提高解题能力。
![](https://i-blog.csdnimg.cn/direct/40e93942614b4212815a10f8a3197b91.jpeg)
一、引言
蓝桥杯全国软件和信息技术专业人才大赛是国内知名的IT类竞赛,C语言是其中重要的竞赛语言之一。动态规划作为一种高效的算法思想,在蓝桥杯C语言竞赛中频繁出现,解决了很多复杂的优化问题。掌握动态规划对于参赛选手来说至关重要。
二、动态规划概述
(一)基本概念
动态规划是一种将复杂问题分解为相对简单的子问题进行求解的方法。它通过存储子问题的解,避免重复计算,从而提高算法效率。动态规划适用于具有重叠子问题和最优子结构特性的问题。
(二)基本原理
动态规划的核心是状态转移方程。它通过定义状态和状态之间的关系,将问题分解为多个阶段,每个阶段对应一个子问题。通过求解子问题,逐步推导出原问题的解。
(三)解题步骤
-
明确问题的阶段划分:根据问题的特点,将问题分解为多个阶段,每个阶段对应一个子问题。
-
定义状态:确定每个阶段的状态变量,用于描述子问题的特征。
-
建立状态转移方程:根据问题的性质,推导出状态之间的关系,建立状态转移方程。
-
确定边界条件:明确初始状态和边界条件,作为递推的起点。
-
计算最优解:根据状态转移方程,从初始状态开始,逐步计算每个阶段的状态值,最终得到原问题的最优解。
三、动态规划问题分类及实例分析
(一)线性DP
1.线性DP简介
线性DP是最基本的动态规划类型,其状态转移是线性的,即状态的更新依赖于前一个或几个状态。
2.例题讲解
例题1:破损的楼梯 问题描述:小孟来到了一座高耸的楼梯前,楼梯共有N级台阶,从第0级台阶出发。小孟每次可以迈上1级或2级台阶,但是,楼梯上的某些台阶已经坏了,不能上去。现在,小孟想要到达楼梯的顶端,也就是第N级台阶,但他不能踩到坏了的台阶上。请问有多少种不踩到坏了的台阶上到达顶端的方案数?由于方案数很大,请输出其对1e9+7取模的结果。
解题思路:
-
状态定义 :设状态
dp[i]
表示走到第i级台阶的方案数。 -
状态转移方程 :
dp[i] = dp[i - 1] + dp[i - 2]
,如果i为破损的,则dp[i] = 0
。 -
边界条件 :
dp[0] = 1
,表示在第0级台阶有一种方案。 -
代码实现:
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10, mod = 1e9+7;
int n, m, x, f[N], vis[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
cin >> x;
vis[x] = 1;
}
f[0] = 1;
for(int i = 1; i <= n; i++)
{
if(vis[i]) continue;
f[i] = f[i - 1] + f[i - 2];
f[i] %= mod;
}
cout << f[n] << '\n';
return 0;
}
表格说明 :假设楼梯共有5级台阶,其中第2级台阶是坏的,dp
数组的变化如下表:
台阶 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
dp值 | 1 | 1 | 0 | 1 | 1 | 2 |
3.线性DP的应用场景
线性DP常用于解决具有线性结构的问题,如爬楼梯、数列问题等。这类问题的特点是状态的更新依赖于前一个或几个状态,且状态的顺序是固定的。
(二)二维DP
1.二维DP简介
二维DP是指动态规划的状态是二维的,即状态由两个变量共同决定。它在处理矩阵、序列对等问题时非常有效。
2.例题讲解
例题2:最长公共子序列(LCS) 问题描述:给定两个序列A和B,求它们的最长公共子序列。
解题思路:
-
状态定义 :设
dp[i][j]
表示序列A的前i个元素和序列B的前j个元素的最长公共子序列长度。 -
状态转移方程:
-
如果
A[i] == B[j]
:dp[i][j] = dp[i - 1][j - 1] + 1
-
否则:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
-
-
代码实现:
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 9;
int n, m, a[N], b[N], dp[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= m; ++i)
cin >> b[i];
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= m; ++j)
{
if (a[i] == b[j])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
cout << dp[n][m] << '\n';
return 0;
}
表格说明 :假设序列A为{1, 3, 4, 2, 5}
,序列B为{1, 4, 3, 5, 2}
,dp
数组的变化如下表:
dp | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 1 | 1 | 1 | 1 | 1 |
2 | 1 | 1 | 2 | 2 | 2 |
3 | 1 | 2 | 2 | 2 | 2 |
4 | 1 | 2 | 2 | 2 | 3 |
5 | 1 | 2 | 2 | 3 | 3 |
3.二维DP的应用场景
二维DP常用于处理矩阵、序列对等问题,如最长公共子序列、编辑距离等。这类问题的特点是状态由两个维度共同决定,且状态的更新依赖于前一个或几个状态。
(三)最长上升子序列(LIS)
1.LIS简介
LIS(最长上升子序列)是一个经典的动态规划问题。LIS问题要求在一个给定的序列中找到一个最长的严格递增子序列。
2.例题讲解
例题3:小明的挑战 问题描述 :小明是蓝桥王国的勇士,他决定不断突破自我。蓝桥首席骑士长给他安排了N个对手,他们的战力值分别为a1, a2, ..., an
,且按顺序挡在小明的前方。小明可以选择单挑也可以选择群战,但他只愿意挑战战力值越来越强的对手。请你算算小明最多会挑战多少名对手。
解题思路:
-
状态定义 :设
dp[i]
表示以第i个对手结尾的最长上升子序列的长度。 -
状态转移方程 :对于每个
i
,遍历j
(1 <= j < i
),如果a[i] > a[j]
,则dp[i] = max(dp[i], dp[j] + 1)
。 -
代码实现:
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, a[N], dp[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
int ans = 0;
for (int i = 1; i <= n; ++i)
{
dp[i] = 1;
for (int j = 1; j < i; ++j)
{
if (a[i] > a[j])
dp[i] = max(dp[i], dp[j] + 1);
}
ans = max(ans, dp[i]);
}
cout << ans << '\n';
return 0;
}
表格说明 :假设序列a
为{10, 9, 2, 5, 3, 7, 101, 18}
,dp
数组的变化如下表:
序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
a[i] | 10 | 9 | 2 | 5 | 3 | 7 | 101 | 18 |
dp[i] | 1 | 1 | 1 | 2 | 2 | 3 | 4 | 4 |
3.LIS的应用场景
LIS问题常用于处理序列中的递增子序列问题,如股票交易、任务调度等。这类问题的特点是状态的更新依赖于前一个或几个状态,且状态的顺序是固定的。
(四)背包问题
1.背包问题简介
背包问题是动态规划中非常重要的一类问题,它包括01背包、完全背包、多重背包等。背包问题的基本形式是:给定一组物品,每个物品有重量和价值,确定在不超过背包容量的前提下,如何选择物品以使总价值最大。
2.例题讲解
例题4:01背包问题 问题描述 :有N件物品和一个容量为W的背包。第i件物品的重量是w[i]
,价值是v[i]
。每件物品只能使用一次,求解将哪些物品装入背包可使这些物品的总价值最大。
解题思路:
-
状态定义 :设
dp[i][j]
表示前i件物品在容量为j的背包下的最大价值。 -
状态转移方程:
-
如果不选择第i件物品:
dp[i][j] = dp[i - 1][j]
-
如果选择第i件物品:
dp[i][j] = dp[i - 1][j - w[i]] + v[i]
(前提是j >= w[i]
) -
最终
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])
-
-
代码实现:
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100 + 10;
int n, w, v[N], wgt[N], dp[N][N];
int main()
{
cin >> n >> w;
for (int i = 1; i <= n; ++i)
cin >> wgt[i] >> v[i];
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= w; ++j)
{
dp[i][j] = dp[i - 1][j];
if (j >= wgt[i])
dp[i][j] = max(dp[i][j], dp[i - 1][j - wgt[i]] + v[i]);
}
}
cout << dp[n][w] << '\n';
return 0;
}
表格说明 :假设n = 3
,w = 5
,物品的重量和价值分别为{(2, 3), (3, 4), (4, 5)}
,dp
数组的变化如下表:
dp | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 3 | 3 | 3 | 3 |
2 | 0 | 0 | 3 | 4 | 7 | 7 |
3 | 0 | 0 | 3 | 4 | 7 | 8 |
3.背包问题的应用场景
背包问题广泛应用于资源分配、组合优化等领域。01背包问题是最基本的形式,完全背包和多重背包问题可以通过对01背包问题的扩展来解决。
(五)区间DP
1.区间DP简介
区间DP是一种处理区间问题的动态规划方法。它的状态由区间表示,通过将区间分解为更小的子区间来求解。
2.例题讲解
例题5:石子合并问题 问题描述:在一个圆形操场上有N堆石子,现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并,合并的代价为这两堆石子的总数。求将这N堆石子合并成一堆的最小代价。
解题思路:
-
状态定义 :设
dp[i][j]
表示将第i堆到第j堆石子合并的最小代价。 -
状态转移方程:
dp[i][j] = min(dp[i][k] + dp[k + 1][j] + sum[i][j])
,其中i <= k < j
,sum[i][j]
表示从第i堆到第j堆石子的总和。
-
代码实现:
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100 + 10;
int n, a[N], sum[N], dp[N][N];
int main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= n; ++i)
sum[i] = sum[i - 1] + a[i];
for (int len = 2; len <= n; ++len)
{
for (int i = 1; i <= n - len + 1; ++i)
{
int j = i + len - 1;
dp[i][j] = INT_MAX;
for (int k = i; k < j; ++k)
{
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
}
}
}
cout << dp[1][n] << '\n';
return 0;
}
表格说明 :假设n = 4
,石子堆的重量分别为{4, 1, 1, 4}
,dp
数组的变化如下表:
dp | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | 0 | 5 | 6 | 14 |
2 | 0 | 0 | 2 | 9 |
3 | 0 | 0 | 0 | 5 |
4 | 0 | 0 | 0 | 0 |
3.区间DP的应用场景
区间DP常用于处理区间合并、区间分割等问题。这类问题的特点是状态由区间表示,且状态的更新依赖于子区间的最优解。
四、动态规划的优化技巧
(一)空间优化
动态规划中,空间复杂度常常较高,可以通过滚动数组等方法进行优化。
1.滚动数组
滚动数组是一种将二维数组压缩为一维数组的方法,适用于状态转移仅依赖于前一行或前几行的情况。
2.例题讲解
例题6:01背包问题的空间优化 问题描述:同01背包问题。
解题思路:
-
状态定义 :设
dp[j]
表示容量为j的背包下的最大价值。 -
状态转移方程:
-
如果不选择第i件物品:
dp[j] = dp[j]
-
如果选择第i件物品:
dp[j] = max(dp[j], dp[j - wgt[i]] + v[i])
-
-
代码实现:
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100 + 10;
int n, w, v[N], wgt[N], dp[N];
int main()
{
cin >> n >> w;
for (int i = 1; i <= n; ++i)
cin >> wgt[i] >> v[i];
for (int i = 1; i <= n; ++i)
{
for (int j = w; j >= wgt[i]; --j)
{
dp[j] = max(dp[j], dp[j - wgt[i]] + v[i]);
}
}
cout << dp[w] << '\n';
return 0;
}
(二)时间优化
动态规划的时间复杂度可以通过减少不必要的计算来优化。
1.二分查找
在某些动态规划问题中,可以使用二分查找来优化状态转移。
2.例题讲解
例题7:最长上升子序列的二分优化 问题描述:同LIS问题。
解题思路:
-
状态定义 :设
dp[i]
表示长度为i的最长上升子序列的最小结尾值。 -
状态转移方程:
- 使用二分查找找到第一个大于等于
a[i]
的位置pos
,更新dp[pos] = a[i]
。
- 使用二分查找找到第一个大于等于
-
代码实现:
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, a[N], dp[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
int len = 0;
for (int i = 1; i <= n; ++i)
{
int pos = lower_bound(dp + 1, dp + len + 1, a[i]) - dp;
dp[pos] = a[i];
if (pos > len)
len = pos;
}
cout << len << '\n';
return 0;
}
五、动态规划的常见问题与解决方法
(一)状态定义困难
状态定义是动态规划的关键步骤,如果状态定义不合理,会导致状态转移方程难以建立。
1.解决方法
-
明确问题的阶段划分:根据问题的特点,将问题分解为多个阶段,每个阶段对应一个子问题。
-
选择合适的状态变量:状态变量应能够完整地描述子问题的特征。
(二)状态转移方程复杂
状态转移方程是动态规划的核心,如果状态转移方程过于复杂,会导致代码实现困难。
1.解决方法
-
分析子问题之间的关系:通过分析子问题之间的关系,找出状态之间的依赖关系。
-
简化状态转移方程:通过引入辅助变量或优化算法,简化状态转移方程。
(三)边界条件难以确定
边界条件是动态规划的起点,如果边界条件不正确,会导致整个算法的错误。
1.解决方法
-
明确初始状态:根据问题的特点,确定初始状态的值。
-
检查边界条件:在代码实现中,仔细检查边界条件的处理。
六、结论
动态规划是一种非常重要的算法思想,在蓝桥杯C语言竞赛中应用广泛。通过本文的介绍,读者可以系统地了解动态规划的基本概念、原理及解题步骤,并通过多个经典例题,掌握动态规划在不同问题中的应用方法。希望本文能够帮助参赛选手更好地理解和掌握动态规划,提高解题能力。