文章目录
背包问题
01背包问题
二维
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;//n表示所有物品个数,m表示背包容量
int v[N], w[N];//v表示物品体积,w表示物品价值
int f[N][N];//f表示状态
//f[i][j]表示在前i给物体中,体积不超过j所能装下的最大价值
int main() {
cin >> n >> m;//读入物品个数和背包容量
for (int i = 1; i <= n; i++)//读入所有物品
cin >> v[i] >> w[i];
//状态计算-集合划分
// 将集合划分为选法不含i和选法含i的两个集合
for (int i = 1; i <= n; i++)//枚举所有物品
for (int j = 1; j <= m; j++) {//枚举所有体积
f[i][j] = f[i - 1][j];//1.选法不含i
// 2.选法含i
if (j >= v[i])//保证j>=v[i]才存在
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
一维
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
for (int i = 1; i < n; i++)
cin >> v[i] >> w[i];
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;
}
完全背包问题
//状态划分:第i个物品选k个,要满足k*v[i]<=j
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)//时间复杂度高
for (int j = 0; j <= m; j++)
for (int k = 0; k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + w[i] * k);
cout << f[n][m] << endl;
return 0;
}
优化
#include <iostream>
#include <algorithm>
using namespace std;
int n, m;
int v[N], w[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)//时间复杂度低
for (int j = 1; j <= m; j++) {
f[i][j] = f[i - 1][j]; //搭配不含i
if (j >= v[i])//搭配含i
f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
一维
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
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++)
f[j] = max(f[j], f[j - v[i] + w[i]]);
cout << f[m] << endl;
return 0;
}
多重背包问题
多重背包的暴力写法
数据范围:100
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
int main() {
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 = 0; j <= m; 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]] + w[i * k]);
cout << f[n][m] << endl;
return 0;
}
优化
数据范围:2000
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 25000, M = 2010;//2000*log2000
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
int cnt = 0;//表示所有新的物品的编号
for (int i = 1; i <= n; i++) {
int a, b, s;
cin >> a >> b >> s;//表示当前第i个物品的体积,价值和个数
int k = 1; //从1开始分
while (k <= s) {
cnt++;//当前物品的编号++
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;//k是1,2,4,8,16,...,
}
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 <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> s[i]; //读入每组
for (int j = 0; j < s[i]; j++)
cin >> v[i][j] >> w[i][j]; //读入第i组的第j个物品的体积与价值
}
for (int i = 1; i <= n; i++)//从前往后枚举每一组
for (int j = m; j >= 0; j--)//从大到小枚举所有体积
for (int k = 0; k < s[i]; k++)//枚举所有选择
if (v[i][k] <= j)
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
cout << f[m] << endl;
return 0;
}
线性DP
数字三角形
//线性DP
//数字三角形
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N];//表示数字三角形的每个点
int f[N][N];//f表示状态
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
scanf("%d", &a[i][j]);
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];
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;
}
最长上升子序列
//线性DP
//最长上升子序列I
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5010;
int n;
int a[N], f[N];
//f[i]是所有以第i个数结尾的上升子序列长度的最大值
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;//以i为结尾的上升子序列的长度是1,只有a[i]一个数
for (int j = 1; j < i; j++)//从1开始枚举上一个数是那个数
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;
}
最长公共子序列
//线性DP
//f[i][j]表示所有在第一个序列的前i个字母中出现,
//且在二个序列的前j个字母中出现的子序列的长度的最大值
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main() {
scanf("%d%d", &n, &m);
scanf("%s%s", a + 1, b + 1);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
f[i][j] = max(f[i][j - 1], f[i - 1][j]);//a[i]和b[j]出现一个
if (a[i] == b[j])//a[i]和b[j]都出现且相等
f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);//都不出现的情况+1
}
printf("%d\n", f[n][m]);
return 0;
}
区间DP
石子合并
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n;
int s[N];
int f[N][N];
//状态表示:所有将第i堆石子到第j堆石子合并成一堆石子的合并方式
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];
//区间长度是1,只有一堆石子,合并不需要代价
for (int len = 2; len <= n; len++)//长度从小到大枚举所有状态
for (int i = 1; i + len - 1 <= n; i++) {//枚举起点i
int l = i, r = i + len - 1;//左右端点
f[l][r] = 1e8;
for (int k = l; k < r; k++) //枚举l~r-1
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;
}
数位统计DP
计数问题
//计数问题
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int get(vector<int>num, int l, int r) { //求num这个数在r到l中出现了几次
int res = 0;
for (int i = l; i >= r; i--)
res = res * 10 + num[i];
return res;
}
int power10(int x) {
int res = 1;
while (x--)
res *= 10;
return res;
}
int count(int n, int x) { //求1-n中x出现了几次
if (!n)
return 0;
vector<int>num;
while (n) {
num.push_back(n % 10);
n /= 10;
}
n = num.size();
int res = 0;
for (int i = n - 1 - !x; i >= 0; i--) {
if (i < n - 1) {
res += get(num, n - 1, i + 1) * power10(i);
if (!x)
res -= power10(i);
}
if (num[i] == x)
res += get(num, i - 1, 0) + 1;
else if (num[i] > x)
res += power10(i);
}
return res;
}
int main() {
int a, b;
while (cin >> a >> b, a || b) {
if (a > b)
swap(a, b);
for (int i = 0; i < 10; i++)
cout << count(b, i) - count(a - 1, i) << ' ';
cout << endl;
}
return 0;
}
状态压缩DP
蒙德里安的梦想
//DP后面没写完注释
#include <iostream>
#include <algorithm>
#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) {//只要n或m不等于0,就一直进行
memset(f, 0, sizeof f);
//预处理所有状态是不是不存在连续奇数个0
for (int i = 0; i < 1 << n; i++) {
st[i] = true;//假设当前该状态存在
int cnt = 0;//cnt存储当前连续0的个数
for (int j = 0; j < n; j++)
if (i >> j & 1) {//当前这一位是1,说明上一段已经截至了
if (cnt & 1)//判断上一段的0是否有连续奇数个
st[i] = false;//0有奇数个,说明第i个状态不合法
cnt = 0;//上一段连续0已经结束了,cnt清成0
} else//如果当前这一位是0
cnt++;
if (cnt & 1)//如果最后一段0的个数是奇数的话
st[i] = false;//说明第i个状态不合法
}
//DP过程
f[0][0] = 1;
for (int i = 1 ; i <= m; i++)
for (int j = 0; j < 1 << n; j++)
for (int k = 0; k < 1 << n; k++)
if ((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
最短Hamilton路径
hamilton路径定义:从0到n-1不重不漏地经过每个点恰好一次
f[i][j]表示:所有从0走到j,走过的所有点是i的所有路径长度的最小值
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 20, M = 1 << N;
int n;
int w[N][N];
int f[M][N];
//f[i][j]所有从0走到j,走过的所有点是i的所有路径长度的最小值
int main() {
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> w[i][j];
memset(f, 0x3f, sizeof f);
f[1][0] = 0;
//从0走到0,走过的所有点只有0一个点,也就是第0位上是1,其余所有位是0
for (int i = 0; i < 1 << n; i++)//i和j枚举所有状态
for (int j = 0; j < n; j++)
if (i >> j & 1)//从0走到j,说明i里面一定要包含j
for (int k = 0; k < n; k++)//枚举从那个点转移过来
if ((i - (1 << j)) >> k & 1)//i除去j点后,一定要包含k点
// 第k位一定是1,才可以走到第k位
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
cout << f[(1 << n) - 1][n - 1] << endl;
//走完了所有点,n个1,每一位上都是1
return 0;
}
树形DP
没有上司的舞会
- f[u][0]所有从以u为根的子树中选择,并且不选u这个点的方案
- f[u][1]所有从以u为根的子树中选择,并且选择u这个点的方案
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 6010;
int n;
int happy[N];//每个点的高兴度
int h[N], e[N], ne[N], idx;//邻接表
int f[N][2];
//f[u][0]所有从以u为根的子树中选择,并且不选u这个点的方案
//f[u][1]所有从以u为根的子树中选择,并且选择u这个点的方案
bool has_father[N];//判断一下每个点是否有父节点
void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void dfs(int u) {//递归求一下每个状态
f[u][1] = happy[u];//选择u这个点,需要先加上它的高兴度
for (int i = h[u]; i != -1; i = ne[i]) {//枚举一下u的所有儿子
int j = e[i];//j表示u的每一个儿子
dfs(j);
f[u][0] += max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)//先读入所有点的高兴度
scanf("%d", &happy[i]);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i++) {
int a, b;
scanf("%d%d", &a, &b);//表示b是a的父节点
has_father[a] = true;//a就有父节点b了
add(b, a);
}
int root = 1;
while (has_father[root])//当当前枚举的这个点有父节点,就看下一个点
root++;//直到没有父节点为止
dfs(root);
printf("%d\n", max(f[root][0], f[root][1]));//两种情况取最大值
return 0;
}
记忆化搜索
滑雪
f[i][j]:所有从(i,j)开始滑的路径的长度最大值
#include <iostream>
#include <algorithm>
#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];//引用,所有写v的地方等价于写成f[x][y]
if (v != -1)//如果v!-1表示v已经算过了
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));
// dp[i][j]是求出这个状态的路径长并且返回
printf("%d\n", res);
return 0;
}
`