一、动态规划
动态规划(Dynamic Programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学等领域中使用的,用来解决特定类型问题的算法思想。其核心思想在于解决复杂问题的过程中,把问题分解为相对简单的子问题,通过解决这些子问题来解决整个问题。以下是动态规划的基本思想和特点的详细介绍:
1.数塔问题
问题描述
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
- 有一个r行的数塔,数塔上有若干数字。问从数塔的最高点到底部,在所有的路径中,经过的数字的和最大为多少?
- 如上图,是一个5行的数塔,其中7—3—8—7—5的路径经过数字和最大,为30。
解题思路
-
动态规划的解决方法: 动态规划的方法避免了重复计算相同的子问题。这可以通过创建一个二维数组
dp
来实现,其中dp[i][j]
代表从塔顶到达第i
行第j
个数字时可能获得的最大总和。状态转移方程是:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − 1 ] ) + n u m [ i ] [ j ] dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]) + num[i][j] dp[i][j]=max(dp[i−1][j],dp[i−1][j−1])+num[i][j]表明当前位置的最大路径和等于其正上方和左上方两个位置的最大路径和中的较大者加上当前位置的数字。通过这样的方式,我们可以填充整个
dp
数组,数组的最底行中的最大值就是整个数塔问题的解。
结论
- 自顶向下的分析
- 自底向上的计算
代码实现
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<vector<int>> num = {
{7},
{3, 8},
{8, 1, 0},
{2, 7, 4, 4},
{4, 5, 2, 6, 5}
};
int n = num.size(); // 塔的层数,可以通过num的大小动态确定
vector<vector<int>> dp(n, vector<int>(n, 0)); // 创建一个二维dp数组,用于存储到达每个位置的最大路径和
for (int i = 0; i < n; ++i) // 初始化dp数组的最底层
{
dp[n - 1][i] = num[n - 1][i];
}
// 自底向上计算路径最大和
for (int i = n - 2; i >= 0; --i) { // 从倒数第二层开始往上计算
for (int j = 0; j <= i; ++j) { // 第i层有i+1个数字
// 计算到达当前位置的最大路径和
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + num[i][j];
}
}
cout << "The maximum sum path is: " << dp[0][0] << endl;
return 0;
}
2.数塔问题(扩展)——免费馅饼
问题描述
有一条10米长的小路,以小路起点为x轴的原点,小路终点为x=10,则共有x=0~10共计11个坐标点。接下来的n行每行有两个整数 x , T x,T x,T 表示一个馅饼将在第T秒掉落在坐标 x x x 上。同一秒在同一点上可能掉落有多个馅饼。
初始时你站在 x = 5 x=5 x=5 上,每秒可移动1m,最多可接到所在位置1m范围内某一点上的馅饼。比如你站在 x = 5 x=5 x=5 上,就可以接到 x = 4 , 5 , 6 x=4,5,6 x=4,5,6 其中一点上的所有馅饼。问你最多可接到多少个馅饼?
解题思路
这个问题可以看作是一种特殊的动态规划问题,与数塔问题相似,不过这次我们不是在寻找从顶部到底部的最大路径,而是寻找能够接到最多馅饼的路径。我们可以将每一秒钟的可能位置看作状态,然后计算每个状态可以接到的最大馅饼数量。
-
状态定义: 定义 d p [ T ] [ x ] dp[T][x] dp[T][x] 表示在第 T T T 秒时,站在位置 x x x 可以接到的最大馅饼数。这是我们要填充的动态规划表。
-
状态转移:考虑到每一秒我们可以移动至多1m,因此对于 d p [ T ] [ x ] dp[T][x] dp[T][x],我们可以从 d p [ T − 1 ] [ x − 1 ] dp[T-1][x-1] dp[T−1][x−1], d p [ T − 1 ] [ x ] dp[T-1][x] dp[T−1][x] 或 d p [ T − 1 ] [ x + 1 ] dp[T-1][x+1] dp[T−1][x+1] 这三个状态中的任何一个转移过来(如果存在),因为这代表了我们上一秒可能的位置。转移方程为:
d p [ T ] [ x ] = m a x ( d p [ T − 1 ] [ x − 1 ] , d p [ T − 1 ] [ x ] , d p [ T − 1 ] [ x + 1 ] ) + p i e s [ T ] [ x ] dp[T][x] = max(dp[T-1][x-1], dp[T-1][x], dp[T-1][x+1]) + pies[T][x] dp[T][x]=max(dp[T−1][x−1],dp[T−1][x],dp[T−1][x+1])+pies[T][x]
其中 p i e s [ T ] [ x ] pies[T][x] pies[T][x] 是在第 T T T 秒 x x x 位置掉落的馅饼数。 -
初始化:初始时刻( T = 0 T=0 T=0)我们站在 x = 5 x=5 x=5 上,所以 d p [ 0 ] [ 5 ] dp[0][5] dp[0][5] 初始为0(假设在第0秒没有馅饼掉落),其他位置初始化为负无穷,代表不可能到达的状态。
-
边界情况:在实际计算中,需要考虑边界情况,即当 x = 0 x=0 x=0 或 x = 10 x=10 x=10 时,我们只能从 x = 1 x=1 x=1 或 x = 9 x=9 x=9 移动过来,这需要特殊处理。
-
填表计算: 按照时间的顺序,从 T = 1 T=1 T=1 开始依次计算 d p [ T ] [ x ] dp[T][x] dp[T][x] 的值,直到最后一秒。
解题代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<vector<int>> dp(100005, vector<int>(20, 0));
int maxT = 0; // 记录最大的时间,以知道需要计算的秒数
for (int i = 0; i < n; ++i) // 读取馅饼信息,并记录到对应的位置和时间上
{
int x, T;
cin >> x >> T;
dp[T][x + 1]++;
maxT = max(maxT, T);
}
for (int i = maxT - 1; i >= 0; i--) // 自底向上(从倒数第二层开始)
{
for (int j = 1; j <= 11; j++) // 无需边界处理,因为边界被初始化为0不影响结果
{
dp[i][j] = dp[i][j] + max(max(dp[i + 1][j], dp[i + 1][j + 1]), dp[i + 1][j - 1]); // 三者取最大
}
}
cout << "The maximum number of pies caught is: " << dp[0][6] << endl;
return 0;
}
3.最长公共子序列(LCS)
int lengthOfLIS(const vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0; // 处理空数组的情况
vector<int> dp(n, 1); // 初始化dp数组,每个元素代表以对应位置元素结尾的最长递增子序列的长度
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1); // 更新以nums[i]结尾的最长递增子序列长度
}
}
}
return *max_element(dp.begin(), dp.end()); // 返回dp数组中的最大值,即最长递增子序列的长度
}
int main() {
vector<int> nums = {1, 5, 2, 4, 3}; // 定义一个序列
cout << lengthOfLIS(nums) << endl; // 输出最长递增子序列的长度
return 0;
}
二、二分查找
int binarySearch(const vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 避免溢出
if (nums[mid] == target) {
return mid; // 找到目标,返回索引
} else if (nums[mid] < target) {
left = mid + 1; // 调整左边界
} else {
right = mid - 1; // 调整右边界
}
}
return -1;
}
三、递推求解
【过河卒】
vector<int>horseX = { 0, -2, -1, 1, 2, 2, 1, -1, -2 };
vector<int>horseY = { 0, 1, 2, 2, 1, -1, -2, -2, -1 };
vector<vector<long long>>f(40, vector<long long>(40));
vector<vector<long long>>stop(40, vector<long long>(40));
int x, y, hx, hy;
int main() {
cin >> x >> y >> hx >> hy;
x += 2, y += 2, hx += 2, hy += 2;
f[2][1] = 1; // 为了初始化f[2][2]
stop[hx][hy] = 1; // 马拦卒
for (int i = 1; i <= 8; i++) {
stop[hx + horseX[i]][hy + horseY[i]] = 1;
}
for (int i = 2; i <= x; i++) {
for (int j = 2; j <= y; j++) {
if (stop[i][j]) continue; // 被马拦住就直接跳过(0)
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
cout << f[x][y];
return 0;
}
四、高精度加法/乘法
#include <algorithm>
highPrecisionAddition(const string &num1, const string &num2) {
string result;
int carry = 0; // 进位
int i = num1.length() - 1, j = num2.length() - 1;
while (i >= 0 || j >= 0 || carry) {
int sum = carry;
if (i >= 0) sum += num1[i--] - '0';
if (j >= 0) sum += num2[j--] - '0';
result.push_back(sum % 10 + '0');
carry = sum / 10;
}
reverse(result.begin(), result.end());
return result;
}
string highPrecisionMultiplication(const string &num1, const string &num2) {
vector<int> result(num1.size() + num2.size(), 0);
for (int i = num1.size() - 1; i >= 0; i--) {
for (int j = num2.size() - 1; j >= 0; j--) {
int mul = (num1[i] - '0') * (num2[j] - '0');
int sum = mul + result[i + j + 1];
result[i + j + 1] = sum % 10;
result[i + j] += sum / 10;
}
}
string strResult;
for (int num : result) {
if (!(strResult.empty() && num == 0)) { // 跳过前导0
strResult.push_back(num + '0');
}
}
return strResult.empty() ? "0" : strResult;
}