数据结构、算法总述:数据结构/算法 C/C++-CSDN博客
目录
动态规划(Dynamic Programming,DP)
概念:
过将复杂问题分解为简单的子问题来求解决策过程的最优化。
核心思想是分解子问题并存储子问题的答案以减少重复计算。
方法:
背包问题
- 01背包每件物品只能装一次
- 完全背包每件物品可以装无限次
- 多重背包每件物品只能装有限次(多次)
- 分组背包每组只能选择一件物品装入(01背包升级)
01背包问题
题目
有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。
基本思路
每种物品仅有一件,可以选择放或不放。
#include <iostream>
#include <vector>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N]; // 体积和价值数组
int dp[N][N]; // dp[i][j] 表示考虑前i件物品,且背包容量为j时的最大价值
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
// 初始化dp数组,可以不初始化,因为下面会覆盖
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
dp[i][j] = 0;
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (v[i] > j) {
dp[i][j] = dp[i-1][j]; // 不装第i件物品
} else {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]); // 装或不装第i件物品,取最大值
}
}
}
cout << dp[n][m] << endl; // 输出最大价值
return 0;
}
使用滚动数组,倒序遍历优化
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N]; // v代表体积,w代表价值
int dp[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
// 省略了cin语句,因为我们已经提供了示例值
}
for (int i = 1; i <= n; i++) {
for (int j = m; j >= v[i]; j--) { // j代表背包容量,滚动数组必须倒序遍历
dp[j] = max(dp[j], dp[j - v[i]] + w[i]); // 滚动数组
}
}
cout << dp[m] << endl; // 输出最后的一个一定是最大的
return 0;
}
状态转移方程:
f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w)
完全背包问题
题目
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是v[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。
基本思路
每件物品可以装无限次
#include <iostream>
using namespace std;
const int N = 1010;
int v[N], w[N];
int dp[N];
int main() {
int n, m;
cin >> n >> m;
// 初始化v[i]和w[i]
for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= m; j++) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
cout << dp[m] << endl;
return 0;
}
f[i][j] = max(f[i - 1][j], f[i][j - v] + w)
多重背包问题
题目
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件体积是v[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。
基本思路
多重背包每件物品只能装有限次(多次)
#include <iostream>
using namespace std;
const int N = 1100;
int f[N][N], v[N], w[N], s[N];
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i] >> s[i];
}
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) { // 从大到小遍历背包容量
for (int k = 0; k <= s[i] && k * v[i] <= j; k++) {
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i] * k] + w[i] * k);//k为第i个物品的个数
二进制拆分优化
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N];
int f[M];
int main()
{
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; i ++ )
{
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while (k <= s)
{
cnt ++ ;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if (s > 0)
{
cnt ++ ;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;
for (int i = 1; i <= n; i ++ )
for (int j = m; j >= v[i]; j -- )
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
单调队列优化
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 20010;
int f[N], g[N], q[N];
int n, m;
int v, w, s;
int main()
{
cin >> n >> m;
for (int i = 0; i < n; ++i)
{
cin >> v >> w >> s;
memcpy(g, f, sizeof f);
for (int j = 0; j < v; ++j)
{
int hh = 0, tt = -1;
for (int k = j; k <= m; k += v)
{
if (hh <= tt && k - s * v > q[hh]) hh++;
while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt--;
q[++tt] = k;
f[k] = g[q[hh]] + (k - q[hh]) / v * w;
}
}
}
cout << f[m] << endl;
return 0;
}
分组背包问题
题目
有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。
基本思路
每组只能选择一件物品装入
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int f[N];
int v[N][N], w[N][N], s[N];
int n, m, k;
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
{
cin >> s[i];
for (int j = 0; j < s[i]; j++)
cin >> v[i][j] >> w[i][j];
}
for (int i = 0; i < n; i++)
{
for (int j = m; j >= 0; j--)
{
for (int k = 0; k < s[i]; k++) //for(int k=s[i];k>=1;k--)也可以
{
if (j >= v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
cout << f[m] << endl;
}
状态转移方程:
f[j] = max(f[j], f[j - v[i][k]] + w[i][k])
线性dp
数字三角形
题目
给定一个数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
#include<iostream>
using namespace std;
// 定义常量N为矩阵的大小,INF为极大值,这里设为1e9,即10^9
const int N=510,INF=1e9;
int n;
int a[N][N];
int f[N][N];
int main()
{
scanf("%d",&n);
// 读入矩阵a的值,注意只读入下三角的部分,因为矩阵是对称的
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
scanf("%d",&a[i][j]);
// 初始化动态规划数组f,对角线和边界设为极大值INF
for(int i=0;i<=n;i++)
for(int j=0;j<=i+1;j++)
f[i][j]=-INF;
f[1][1]=a[1][1];
// 动态规划求解,更新f数组
for(int i=2;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
int res=-INF;
for(int i=1;i<=n;i++)
res=max(res,f[n][i]);
printf("%d",res);
return 0;
}
状态转移方程:
f[i][j] = max(f[i-1][j-1]+a[i][j], f[i-1][j]+a[i][j])
最长上升子序列
题目
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
#include<iostream>
using namespace std;
const int N = 1010;
int n;
int a[N], f[N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 1; i <= n; i++)
{
f[i] = 1; // 只有a[i]一个数
for (int j = 1; j <= i; j++)
if (a[j] < a[i])
f[i] = max(f[i], f[j] + 1);
}
int res = 0;
for (int i = 1; i <= n; i++)
res = max(res, f[i]);
printf("%d\n", res);
return 0;
}
if(a[j] < a[i]) f[i] = max(f[i], f[j] + 1)
二分优化
#include<iostream>
using namespace std;
const int N = 100010;
int n;
int a[N];
int q[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
int len = 0;
for (int i = 0; i < n; i++)
{
int l = 0, r = len;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (q[mid] < a[i])
l = mid;
else
r = mid - 1;
}
len = max(len, r + 1);
q[r + 1] = a[i]; // 替换或添加
}
printf("%d\n", len);
return 0;
}
最长公共子序列
题目
给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少
#include<iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
cin >> n >> m >> a + 1 >> b + 1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (a[i] == b[j])
f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
}
cout << f[n][m] << endl;
return 0;
}
状态转移方程:
f[i][j] = max(f[i-1][j], f[i][j-1]);
if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
最短编辑距离
题目
给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:
- 删除–将字符串 A 中的某个字符删除。
- 插入–在字符串 A 的某个位置插入某个字符。
- 替换–将字符串 A 中的某个字符替换为另一个字符。
现在请你求出,将 A 变为 B 至少需要进行多少次操作。
#include<iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
scanf("%d%s", &n, a + 1);
scanf("%d%s", &m, b + 1);
for (int i = 0; i <= m; i++)
f[0][i] = i;
for (int i = 0; i <= n; i++)
f[i][0] = i; // 初始化字符串的编辑操作
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
if (a[i] == b[j])
f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else
f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1); // 状态转移方程
}
}
printf("%d\n", f[n][m]);
return 0;
}
状态转移方程:
f[i][j] = min(f[i-1][j]+1, f[i][j-1]+1);
if(a[i] == b[j]) f[i][j] = min(f[i][j], f[i-1][j-1]);
else f[i][j] = min(f[i][j], f[i-1][j-1]+1);
区间dp
石子合并
题目
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子 将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
#include<iostream>
using namespace std;
const int N = 310;
int n;
int s[N];
int f[N][N]; // 状态表示:集合f[l][r]为[l,r]区间;属性:所堆成的最小值
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &s[i]);
for (int i = 1; i <= n; i++)
s[i] += s[i - 1]; // 前缀和用来求一段区间的和
for (int len = 2; len <= n; len++) // 区间长度为len//枚举长度
for (int i = 1; i + len - 1 <= n; i++) // 意思就是i在区间[1,n-len+1]中去//枚举区间
{
int l = i, r = i + len - 1; // 区间在[i,i+len-1]中间长度为len//设置l和r的区间
f[l][r] = 1e9; // 初始化最大值
for (int k = l; k < r; k++) // 枚举分界点//不取r
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
printf("%d\n", f[1][n]);
return 0;
}
状态转移方程为:
f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r]-s[l-1])
计数类dp
整数划分
题目
一个正整数 n 可以表示成若干个正整数之和,我们将这样的一种表示称为正整数 n 的一种划分。 现在给定一个正整数 n,请你求出 n共有多少种不同的划分方法。
#include <iostream>
using namespace std;
const int M = 1e9 + 7;
int f[1010], n;
int main()
{
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
f[j] = (f[j - i] + f[j]) % M;
cout << f[n] << endl;
return 0;
}
状态转移方程:
f[j] = (f[j-i] + f[j])
数位统计DP
题目
给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9的出现次数。
#include<iostream>
#include<cmath>
using namespace std;
int dgt(int n) // 计算整数n有多少位
{
int res = 0;
while (n) ++ res, n /= 10;
return res;
}
int cnt(int n, int i) // 计算从1到n的整数中数字i出现多少次
{
int res = 0, d = dgt(n);
for (int j = 1; j <= d; ++ j) // 从右到左第j位上数字i出现多少次
{
// l和r是第j位左边和右边的整数; dj是第j位的数字
int p = pow(10, j - 1), l = n / p / 10, r = n % p, dj = n / p % 10;
// 计算第j位左边的整数小于l的情况
if (i) res += l * p;
if (!i && l) res += (l - 1) * p; // 如果i = 0, 左边高位不能全为0
// 计算第j位左边的整数等于l情况
if ( (dj > i) && (i || l) ) res += p;
if ( (dj == i) && (i || l) ) res += r + 1;
}
return res;
}
int main()
{
int a, b;
while (cin >> a >> b , a)
{
if (a > b) swap(a, b);
for (int i = 0; i <= 9; ++ i) cout << cnt(b, i) - cnt(a - 1, i) << ' ';
cout << endl;
}
return 0;
}
状态压缩DP
蒙德里安的梦想
题目
n×m 的棋盘可以摆放不同的1×2小方格的种类数。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 12, M = 1 << N;
int n, m;
long long f[N][M];
bool st[M];
int main()
{
int n, m;
while (cin >> n >> m, n || m)
{
memset(f, 0, sizeof f);
// 预处理:判断合并列的状态i是否合法
// 如果合并列的某行是1表示横放,是0表示竖放
// 如果合并列不存在连续的奇数个0,即为合法状态
for (int i = 0; i < 1 << n; i++)
{
st[i] = true;
int cnt = 0; // 记录合并列中连续0的个数
for (int j = 0; j < n; j++)
{
if (i >> j & 1) // 如果是1
{
if (cnt & 1) // 如果连续0的个数是奇数
{
st[i] = false; // 记录i不合法
break;
}
} else
cnt++; // 如果是0,记录0的个数
}
if (cnt & 1)
st[i] = false; // 处理高位0的个数
}
// 状态计算
f[0][0] = 1; // 第0列不横放是一种合法的方案
for (int i = 1; i <= m; i++) // 阶段:枚举列
for (int j = 0; j < 1 << n; j++) // 状态:枚举i列的状态
for (int k = 0; k < 1 << n; k++) // 状态:枚举i-1列的状态
// 两列状态兼容:不出现重叠的1,不出现连续奇数个0
if ((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl; // 第m列不横放,既答案
}
return 0;
}
状态转移方程:
if((j & k) == 0 && st[j|k]) f[i][j] += f[i-1][k];
最短Hamilton路径
题目
给定一张 n 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 20, M = 1 << N;
int n;
int w[N][N];
int f[M][N]; // 第一维表示是否访问到该点的压缩状态,第二维是走到点j
// f[i][j]表示状态为i并且到j的最短路径
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) // 读入i到j的距离
cin >> w[i][j];
memset(f, 0x3f, sizeof f);
f[1][0] = 0;
for (int i = 0; i < 1 << n; i++) // 枚举压缩的状态
for (int j = 0; j < n; j++) // 枚举到0~j的点
if (i >> j & 1) // 该状态存在j点
for (int k = 0; k < n; k++) // 枚举从j倒数第二个点k
if (i >> k & 1) // 倒数点k存在
f[i][j] = min(f[i][j], f[i - (1 << j)][k] +w[k][j]); // 状态转移方程,在f[i][j] 和状态去掉j的点f[i - (i << j)][k] + w[k][j] 取最小值
cout<< f[(1 << n) - 1][n - 1]<< endl; // 输出状态全满也就是所有点都经过且到最后一个点的最短距离
return 0;
}
状态转移方程:
f[i][j] = min(f[i][j], f[i-(1<<j)][k] + w[k][j]);
树形DP
题目
Ural 大学有 N 名职员,编号为 1∼N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
#include <iostream>
#include <vector>
using namespace std;
const int N = 6010;
int h[N];
int v[N];
vector<int> son[N];
int f[N][2];
void dp(int x)
{
f[x][0] = 0;
f[x][1] = h[x];
for (int i = 0; i < son[x].size(); i++)
{
int y = son[x][i];
dp(y);
f[x][0] += max(f[y][0], f[y][1]);
f[x][1] += f[y][0];
}
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> h[i];
for (int i = 1; i <= n - 1; i++)
{
int x, y;
cin >> x >> y;
son[y].push_back(x);
v[x] = 1;
}
int root;
for (int i = 1; i <= n; i++)
if (!v[i])
{
root = i;
break;
}
dp(root);
cout << max(f[root][0], f[root][1]) << endl;
return 0;
}
记忆化搜索
题目
给定一个R 行 C 列的矩形网格滑雪场
一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。
当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 310;
int n, m;
int h[N][N];
int f[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int dp(int x, int y)
{
int& v = f[x][y];
if (v != -1) return v; // 记忆化搜索核心
v = 1;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a >= 1 && a <= n && b >= 1 && b <= m && h[a][b] < h[x][y]) // 判断是否越界且上一个经过的点的高度是否大于当前高度
v = max(v, dp(a, b) + 1);
}
return v;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &h[i][j]);
memset(f, -1, sizeof f);
int res = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
res = max(res, dp(i, j));
printf("%d\n", res);
return 0;
}
状态转移方程
v = max(v, dp(a,b) + 1)