背包问题
-
01背包问题:存在一个体积为V的背包和N件物品,每件物品都有v(体积)和w(价值)两种属性,每件物品最多只能使用一次。求解如何把物品装入背包,可使这些物品的总体积不超过背包容量,并且总价值最大
-
完全背包问题:存在一个体积为V的背包和N件物品,每件物品都有v(体积)和w(价值)两种属性,每件物品可以使用无限次。求解如何把物品装入背包,可使这些物品的总体积不超过背包容量,并且总价值最大
-
多重背包问题:存在一个体积为V的背包和N件物品,每件物品都有v(体积)和w(价值)两种属性,每件物品最多可以使用对应给定的个数次。求解如何把物品装入背包,可使这些物品的总体积不超过背包容量,并且总价值最大
-
分组背包问题:存在一个体积为V的背包和N组物品,每组物品有若干件物品,每件物品都有v(体积)和w(价值)两种属性,每一组物品中最多只能选一件物品。求解如何把物品装入背包,可使这些物品的总体积不超过背包容量,并且总价值最大
01背包
01背包问题:存在一个体积为V的背包和N件物品,每件物品都有v(体积)和w(价值)两种属性,每件物品最多只能使用一次。求解如何把物品装入背包,可使这些物品的总体积不超过背包容量,并且总价值最大
01背包
-
状态表示:f[i,j](f[i,j]表示,在,由,只从前i件物品中选并且总体积小于等于j的每种选法的总价值,组成的集合,中,的最大值。f[N,V]即为最终的结果)
-
状态计算:f[i,j]=max(f[i-1,j],f[i-1,j-vi]+wi)(vi表示第i件物品的体积,wi表示第i件物品的价值)
-
朴素版
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n;
int v[N], w[N];// 每件物品都有v(体积)和w(价值)两种属性
int m;
int f[N][M];// 状态表示
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d %d", &v[i], &w[i]);
/*
f[0][0]~f[0][m],根据状态表示的含义,可得这些值都为0,
由于该数组为全局变量,所以已经默认为0,无需手动赋值为0
*/
// 状态计算:f[i,j]=max(f[i-1,j],f[i-1,j-vi]+wi)(vi表示第i件物品的体积,wi表示第i件物品的价值)
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j];
// 当j大于等于第i件物品的体积时,才可以视为选择了第i件物品
if (j >= v[i])
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
printf("%d\n", f[n][m]);
return 0;
}
- 优化版
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n;
int v[N], w[N];// 每件物品都有v(体积)和w(价值)两种属性
int m;
int f[M];
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d %d", &v[i], &w[i]);
// 状态计算:f[i,j]=max(f[i-1,j],f[i-1,j-vi]+wi)(vi表示第i件物品的体积,wi表示第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]);
printf("%d\n", f[m]);
return 0;
}
完全背包
完全背包问题:存在一个体积为V的背包和N件物品,每件物品都有v(体积)和w(价值)两种属性,每件物品可以使用无限次。求解如何把物品装入背包,可使这些物品的总体积不超过背包容量,并且总价值最大
完全背包
-
状态表示:f[i,j](f[i,j]表示,在,由,只从前i件物品中选并且总体积小于等于j的每种选法的总价值,组成的集合,中,的最大值。f[N,V]即为最终的结果)
-
状态计算:f[i,j]=max(f[i-1,j-k*vi]+k*wi)(vi表示第i件物品的体积,wi表示第i件物品的价值,k表示第i件物品使用了k次)
-
朴素版,会超时
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n;
int v[N], w[N];// 每件物品都有v(体积)和w(价值)两种属性
int m;
int f[N][M];// 状态表示
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d %d", &v[i], &w[i]);
/*
f[0][0]~f[0][m],根据状态表示的含义,可得这些值都为0,
由于该数组为全局变量,所以已经默认为0,无需手动赋值为0
*/
// 状态计算:f[i,j]=max(f[i-1,j-k*vi]+k*wi)(vi表示第i件物品的体积,wi表示第i件物品的价值,k表示第i件物品使用了k次)
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]] + k * w[i]);
printf("%d\n", f[n][m]);
return 0;
}
- 优化版
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n;
int v[N], w[N];// 每件物品都有v(体积)和w(价值)两种属性
int m;
int f[N][M];// 状态表示
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d %d", &v[i], &w[i]);
/*
f[0][0]~f[0][m],根据状态表示的含义,可得这些值都为0,
由于该数组为全局变量,所以已经默认为0,无需手动赋值为0
*/
/*
观察下面两个状态计算:
f[i,j]=max(f[i-1,j],f[i-1,j-1*vi]+1*wi,f[i-1,j-2*vi]+2*wi,f[i-1,j-3*vi]+3*wi,...)
f[i,j-vi]=max( f[i-1,j-1*vi] ,f[i-1,j-2*vi]+1*wi,f[i-1,j-3*vi]+2*wi,...)
可得如下状态计算:
f[i,j]=max(f[i-1,j],f[i,j-vi]+wi)(vi表示第i件物品的体积,wi表示第i件物品的价值)
*/
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j];
if (j >= v[i])
f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
printf("%d\n", f[n][m]);
return 0;
}
- 再次优化版
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n;
int v[N], w[N];// 每件物品都有v(体积)和w(价值)两种属性
int m;
int f[M];
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d %d\n", &v[i], &w[i]);
// 状态计算:f[i,j]=max(f[i-1,j],f[i,j-vi]+wi)(vi表示第i件物品的体积,wi表示第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]);
printf("%d\n", f[m]);
return 0;
}
多重背包
多重背包问题:存在一个体积为V的背包和N件物品,每件物品都有v(体积)和w(价值)两种属性,每件物品最多可以使用对应给定的个数次。求解如何把物品装入背包,可使这些物品的总体积不超过背包容量,并且总价值最大
多重背包
- 状态表示:f[i,j](f[i,j]表示,在,由,只从前i件物品中选并且总体积小于等于j的每种选法的总价值,组成的集合,中,的最大值。f[N,V]即为最终的结果)
- 状态计算:f[i,j]=max(f[i-1,j-k*vi]+k*wi)(vi表示第i件物品的体积,wi表示第i件物品的价值,k表示第i件物品使用了k次)
多重背包问题 I
#include <iostream>
using namespace std;
const int N = 110;
const int M = 110;
int n;
int v[N], w[N], s[N];// 每件物品都有v(体积)和w(价值)和s(个数)三种属性
int m;
int f[N][M];// 状态表示
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d %d %d", &v[i], &w[i], &s[i]);
/*
f[0][0]~f[0][m],根据状态表示的含义,可得这些值都为0,
由于该数组为全局变量,所以已经默认为0,无需手动赋值为0
*/
// 状态计算:f[i,j]=max(f[i-1,j-k*vi]+k*wi)(vi表示第i件物品的体积,wi表示第i件物品的价值,k表示第i件物品使用了k次)
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
for (int k = 0; k * v[i] <= j && k <= s[i]; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
printf("%d\n", f[n][m]);
return 0;
}
多重背包问题 II
#include <iostream>
using namespace std;
const int N = 12000;// 1000*log2000小于11000(这里的log2000是以2为底的)
const int M = 2010;
int n;
int v[N], w[N];// 把多重背包问题转化为了01背包问题,每件物品都有v(体积)和w(价值)两种属性
int m;
int f[M];
/*
解题思路:
1、存在n件物品
2、假设第i件物品的个数为si,
把si用如下方式表示,
si=2^0+2^1+2^2+2^3+...+2^k+c(其中,c=si-(2^(k+1)-1)),
把si个第i件物品作拆分,
拆分成2^0个第i件物品,2^1个第i件物品,2^2个第i件物品,2^3个第i件物品,...,2^k个第i件物品,c个第i件物品,
即把si个第i件物品拆分成了k+2份不同数量的第i件物品,
可得,对这k+2份不同数量的第i件物品的所有选法(每一份只有选或不选,两种选择),等于对未拆分前的si个第i件物品的所有选法,
对未拆分前的si个第i件物品的所有选法为:
选择了0个第i件物品,选择了1个第i件物品,选择了2个第i件物品,...,选择了si个第i件物品,
即可以把这k+2份不同数量的第i件物品分别视为新的一件物品,每件物品只有选或不选,两种选择
3、按照步骤2,以此类推,把剩下的n-1件物品作同样的处理
4、则原来的n件物品会变成新的若干件物品,每件物品只有选或不选,两种选择
5、即把多重背包问题转化为了01背包问题
*/
int main()
{
scanf("%d %d", &n, &m);
int cnt = 0;
// 循环n次,对n件物品分别作拆分
while (n--)
{
int a, b, c;
scanf("%d %d %d", &a, &b, &c);// 体积,价值,个数
int k = 1;
while (k <= c)
{
cnt++;
v[cnt] = k * a;
w[cnt] = k * b;
c -= k;
k *= 2;
}
if (c > 0)
{
cnt++;
v[cnt] = c * a;
w[cnt] = c * b;
}
}
// 原来的n件物品会变成新的若干件物品,每件物品只有选或不选,两种选择
n = cnt;// 把多重背包问题转化为了01背包问题
// 01背包问题的状态计算:f[i,j]=max(f[i-1,j],f[i-1,j-vi]+wi)(vi表示第i件物品的体积,wi表示第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]);
printf("%d\n", f[m]);
return 0;
}
分组背包
分组背包问题:存在一个体积为V的背包和N组物品,每组物品有若干件物品,每件物品都有v(体积)和w(价值)两种属性,每一组物品中最多只能选一件物品。求解如何把物品装入背包,可使这些物品的总体积不超过背包容量,并且总价值最大
分组背包
- 状态表示:f[i,j](f[i,j]表示,在,由,只从前i组物品中选并且总体积小于等于j的每种选法的总价值,组成的集合,中,的最大值。f[N,V]即为最终的结果)
- 状态计算:f[i,j]=max(f[i-1,j-v[i,k]]+w[i,k])(v[i,k]表示第i组物品中的第k件物品的体积,w[i,k]表示第i组物品中的第k件物品的价值,当k为0时则表示第i组物品中不选择任何一件物品)
#include <iostream>
using namespace std;
const int N = 110;
const int M = 110;
int n;
int s[N];
int v[N][N], w[N][N];// 存在N组物品,每组物品有若干件物品,每件物品都有v(体积)和w(价值)两种属性
int m;
int f[M];
int main()
{
scanf("%d %d", &n, &m);
// 存在N组物品
for (int i = 1; i <= n; i++)
{
// 每组物品有若干件物品
scanf("%d", &s[i]);
// 每件物品都有v(体积)和w(价值)两种属性
for (int j = 0; j < s[i]; j++)
scanf("%d %d", &v[i][j], &w[i][j]);
}
// 状态计算:f[i,j]=max(f[i-1,j-v[i,k]]+w[i,k])(v[i,k]表示第i组物品中的第k件物品的体积,w[i,k]表示第i组物品中的第k件物品的价值,当k为0时则表示第i组物品中不选择任何一件物品)
// 状态计算的过程可以用一维数组去优化
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]);
printf("%d\n", f[m]);
return 0;
}
线性DP
数字三角形
- 状态表示:f[i,j](f[i,j]表示,在,由,从起点走到(i,j)这个结点的每条路径的路径上所有结点的数字之和,组成的集合,中,的最大值)
- 状态计算:f[i,j]=max(f[i-1,j-1]+a[i,j],f[i-1,j]+a[i,j])(a[i,j]表示(i,j)这个结点的数字)
数字三角形
#include <iostream>
using namespace std;
const int N = 510;
const int INF = 1e9;
int n;
int a[N][N];
int f[N][N];// 状态表示
int main()
{
scanf("%d", &n);
// 初始化每个结点的数字
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
scanf("%d\n", &a[i][j]);
// 结合状态计算分析,因为存在左上方没有结点的结点和存在右上方没有结点的结点,因此要对边界作预处理
for (int i = 1; i <= n; i++)
for (int j = 0; j <= i + 1; j++)
f[i][j] = -INF;
// (1,1)这个结点为起点,根据状态表示的含义,可得f[1][1]=a[1][1]
f[1][1] = a[1][1];
// 状态计算:f[i,j]=max(f[i-1,j-1]+a[i,j],f[i-1,j]+a[i,j])(a[i,j]表示(i,j)这个结点的数字)
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 j = 1; j <= n; j++)
res = max(res, f[n][j]);
printf("%d\n", res);
return 0;
}
最长上升子序列
- 状态表示:f[i](f[i]表示,在,由,以第i个数结尾的每个上升子序列的长度,组成的集合,中,的最大值)
- 状态计算:f[i]=max(f[j]+1)(其中,j∈{1,2,…,i-1})
最长上升子序列
#include <iostream>
using namespace std;
const int N = 1010;
int n;
int a[N];
int f[N];// 状态表示
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d\n", &a[i]);
// 状态计算:f[i]=max(f[j]+1)(其中,j∈{1,2,...,i-1})
for (int i = 1; i <= n; i++)
{
f[i] = 1;// 以第i个数结尾的每个上升子序列的长度最短为1
for (int j = 1; j <= n - 1; 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;
}
最长公共子序列
- 状态表示:f[i,j](f[i,j]表示,在,由,在第一个序列的前i个字母中出现且在第二个序列的前j个字母中出现的每个公共子序列的长度,组成的集合,中,的最大值)
- 状态计算:f[i,j]=max(f[i-1,j-1],f[i-1,j],f[i,j-1],f[i-1,j-1]+1)=max(f[i-1,j],f[i,j-1],f[i-1,j-1]+1)(划分集合时,如果集合的属性为最大值,则各个子集合间允许存在交集)
最长公共子序列
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n, m;
char a[N], b[M];
int f[N][M];
int main()
{
scanf("%d %d", &n, &m);
scanf("%s %s", a + 1, b + 1);
// 状态计算:f[i,j]=max(f[i-1,j-1],f[i-1,j],f[i,j-1],f[i-1,j-1]+1)=max(f[i-1,j],f[i,j-1],f[i-1,j-1]+1)(划分集合时,如果集合的属性为最大值,则各个子集合间允许存在交集)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
f[i][j] = f[i - 1][j];
f[i][j] = max(f[i][j], f[i][j - 1]);
if (a[i] == b[j])// 公共子序列
f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
printf("%d\n", f[n][m]);
return 0;
}
区间DP
石子合并
- 状态表示:f[i,j](f[i,j]表示,在,由,将第i堆石子到第j堆石子合并成一堆石子的每个合并方式的代价,组成的集合,中,的最小值)
- 状态计算:f[i,j]=min(f[i,k]+f[k+1,j]+s[j]-s[i-1])(s[j]表示第1堆石子到第j堆石子的总重量,s[i-1]表示第1堆石子到第i-1堆石子的总重量,s[j]-s[i-1]表示第i堆石子到第j堆石子的总重量,k∈{i,i+1,i+2,…,j-1})
石子合并
#include <iostream>
using namespace std;
const int N = 310;
int n;
int s[N];// 前缀和数组
int f[N][N];// 状态表示
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", &s[i]);
// 前缀和
s[i] += s[i - 1];
}
// 按区间长度从小到大进行遍历
for (int len = 2; len <= n; len++)// 区间长度从2开始
{
// 遍历当前区间长度的左边界
for (int t = 1; t + len - 1 <= n; t++)
{
int l = t;// 左边界
int r = t + len - 1;// 右边界
f[l][r] = 1e9;
/*
状态计算:f[i,j]=min(f[i,k]+f[k+1,j]+s[j]-s[i-1])
(s[j]表示第1堆石子到第j堆石子的总重量,
s[i-1]表示第1堆石子到第i-1堆石子的总重量,
s[j]-s[i-1]表示第i堆石子到第j堆石子的总重量,
k∈{i,i+1,i+2,...,j-1})
*/
for (int k = l; k <= r - 1; k++)
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>
using namespace std;
const int MOD = 1e9 + 7;
const int N = 1010;
int f[N];
/*
解题思路:
1、把正整数n看成是体积为n的背包,把正整数1~n看成是N件物品,每件物品的体积为1,2,3,...,i,...,n
2、此时,即可把问题转化为背包问题:
存在一个体积为n的背包和N件物品,每件物品的体积为1,2,3,...,i,...,n,每件物品可以使用无限次。
求解从第1~N件物品中选,并且总体积恰好等于n的组合数
3、背包问题:
状态表示:f[i,j](f[i,j]表示,从第1~i件物品中选,并且总体积恰好等于j,的组合数。f[N,n]即为最终的结果)
状态计算:f[i,j]=f[i-1,j]+f[i-1,j-1*i]+f[i-1,j-2*i]+f[i-1,j-3*i]+...
4、观察下面两个状态计算:
f[i,j]=f[i-1,j]+f[i-1,j-1*i]+f[i-1,j-2*i]+f[i-1,j-3*i]+...
f[i,j-i]= f[i-1,j-1*i]+f[i-1,j-2*i]+f[i-1,j-3*i]+...
5、可得如下状态计算:
f[i,j]=f[i-1,j]+f[i,j-i]
*/
int main()
{
int n;
scanf("%d", &n);
int volume = n;// 把正整数n看成是体积为n的背包
int count = n;// 把正整数1~n看成是N件物品
// f[1][0],f[2][0],f[3][0],...,f[count][0]的值都是1,因为要让总体积恰好等于0,则表示什么物品都不选,就是1种方案
f[0] = 1;
// 状态计算:f[i,j]=f[i-1,j]+f[i,j-i]
// 状态计算的过程可以用一维数组去优化
for (int i = 1; i <= count; i++)
/*
这里用到了滚动数组的核心思想:
用上一层的旧数据算出新数据,
然后用新数据覆盖当前层的旧数据,
以此类推再继续计算
(上一层和当前层对应的是同一层下的两种不同状态)
*/
// 第i件物品的体积为i
for (int j = i; j <= volume; j++)
f[j] = (f[j] + f[j - i]) % MOD;
printf("%d\n", f[volume]);
return 0;
}
- 第二种解法
#include <iostream>
using namespace std;
const int MOD = 1e9 + 7;
const int N = 1010;
int f[N][N];// 状态表示
int main()
{
int n;
scanf("%d", &n);
// 0被恰好表示成0个正整数的和,的组合数为1
f[0][0] = 1;
/*
解题思路:
状态表示:f[i,j](f[i,j]表示,正整数i被恰好表示成j个正整数的和,的组合数)
状态计算:f[i,j]=f[i-1][j-1]+f[i-j][j](其中j<=i,因为正整数i最多只能被表示成i个正整数的和,即i个1的和)
*/
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)// j<=i,因为正整数i最多只能被表示成i个正整数的和,即i个1的和
f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % MOD;
int res = 0;
for (int j = 1; j <= n; j++)
res = (res + f[n][j]) % MOD;
printf("%d\n", res);
return 0;
}
数位统计DP
#include <iostream>
#include <vector>
using namespace std;
// 返回num数组中下标从l到r(从高位到低位)的每一位数字所构成的数值
int get(vector<int> num, int l, int r)
{
int res = 0;
for (int i = l; i >= r; i--)
res = res * 10 + num[i];
return res;
}
// 返回10的n次方的结果
int power10(int n)
{
int res = 1;
while (n--)
res *= 10;
return res;
}
/*
这个count()函数的思路:
1、假设n的数值为abcdefg
2、要求在1~abcdefg中,数字x出现的次数,
则只需要求出数字x在每一位上出现的次数,
然后把这些次数相加求和即可
3、举个例子,abcxefg,要求数字x在当前位上出现的次数,
分情况讨论(当数字x在最高位时则不存在第1种情况。当数字x为0时则第2种情况不能从最高位开始):
1、当高位的前三位取值为000~abc-1时,
低位的后三位可取值的范围为000~999,
则数字x在当前位上可出现的次数为abc*1000。
如果数字x为0,则高位的前三位取值不能为000
2、当高位的前三位取值为abc时,
分情况讨论:
1、当d等于数字x时,
低位的后三位可取值的范围为000~efg,
则数字x在当前位上可出现的次数为efg+1
2、当d大于数字x时,
低位的后三位可取值的范围为000~999,
则数字x在当前位上可出现的次数为1000
把这两种情况的数字x在当前位上可出现的次数相加求和,
即为数字x在当前位上出现的次数
*/
int count(int n, int x)// 返回在1~n中,数字x出现的次数
{
vector<int> num;
// 先把n中从低位到高位的每一位数字依次放入到num数组中
while (n)
{
num.push_back(n % 10);
n /= 10;
}
int res = 0;
int len = num.size();
// 求出数字x在每一位上出现的次数
for (int i = len - 1 - !x; i >= 0; i--)// 当数字x为0时则第2种情况不能从最高位开始
{
// 第1种情况
if (i <= len - 2)// 当数字x在最高位时则不存在第1种情况
{
res += get(num, len - 1, i + 1) * power10(i);
if (!x)// 如果数字x为0,则高位的取值不能为0
res -= power10(i);
}
// 第2种情况
if (num[i] == x)
res += get(num, i - 1, 0) + 1;
if (num[i] > x)
res += power10(i);
}
return res;
}
int main()
{
int a, b;
while (cin >> a >> b, a || b)// 当a和b都为0时,则结束循环
{
if (a > b)
swap(a, b);
for (int i = 0; i <= 9; i++)
/*
count(b,i)表示在1~b中,数字i出现的次数,
count(a,i)表示在1~a中,数字i出现的次数,
count(b,i)-count(a-1,i)表示在a~b中,数字i出现的次数
*/
printf("%d ", count(b, i) - count(a - 1, i));
puts("");
}
return 0;
}
状态压缩DP
蒙德里安的梦想
-
状态表示:f[i,j](f[i,j]表示,从第i-1列捅到第i列的1x2长方形所对应的行的状态为j时,摆放好前i-1列的1x2长方形,的组合数)
举个例子:假设存在5行10列的棋盘,f[8,26]表示,从第7列捅到第8列的1x2长方形所对应的行为第1行、第2行、第4行,二进制表示为11010,十进制表示为26时,摆放好前7列的1x2长方形,的组合数
-
状态计算:f[i,j]=sum(f[i-1,k])(其中,k∈{0,1,2,3,…,2^(棋盘的行数)-1},并且k需要满足两个条件:1、j&k(位运算)的结果要等于0;2、二进制表示中的j|k(位运算)的结果,不存在连续奇数个0的情况)
蒙德里安的梦想
#include <iostream>
#include <cstring>
using namespace std;
const int ROW = 15, COL = 15;
const int ROW_STATE = 1 << ROW;
long long f[COL][ROW_STATE];// 状态表示
bool st[ROW_STATE];
int main()
{
int r, c;
while (cin >> r >> c, r || c)// 当r和c都为0时,则结束循环
{
// 预处理每个行的状态所对应的二进制表示的数,是否存在连续奇数个0
for (int i = 0; i < 1 << r; i++)
{
st[i] = true;
int cnt = 0;
// 枚举i所对应的二进制表示的数的每一位
for (int j = 0; j < r; j++)
// 每次遇到一个1,就需要判断上一次统计的连续的0的个数
if (i >> j & 1)
{
if (cnt & 1)// 判断cnt是否为奇数
{
st[i] = false;// 存在连续奇数个0
break;
}
cnt = 0;
}
else
cnt++;
if (cnt & 1)// 判断cnt是否为奇数
st[i] = false;// 存在连续奇数个0
}
memset(f, 0, sizeof f);
// 根据状态表示的含义可得
f[0][0] = 1;
// 状态计算
for (int i = 1; i <= c; i++)
for (int j = 0; j < 1 << r; j++)
for (int k = 0; k < 1 << r; k++)
if ((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
// 输出结果
printf("%lld\n", f[c][0]);
}
return 0;
}
最短Hamilton路径
-
状态表示:f[i,j](f[i,j]表示,在,由,从第0个结点走到第j个结点的路径的状态为i的每条路径的路径上所有边的权重之和,组成的集合,中,的最小值)
-
状态计算:f[i,j]=min(f[i-(1<<j),k]+w(k,j))(其中,i-(1<<j)表示路径的状态为i除去第j个结点后的路径的状态,w(k,j)表示从结点k到结点j的权重,i>>j&1(位运算)的结果为1,(i-(1<<j))>>k&1(位运算)的结果为1)
最短Hamilton路径
#include <iostream>
#include <cstring>
using namespace std;
const int N = 21;// 注意,N的上限为21,因为此题有空间限制
const int PATH_STATE = 1 << N;
int f[PATH_STATE][N];// 状态表示
int w[N][N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
scanf("%d", &w[i][j]);
memset(f, 0x3f, sizeof f);
// 根据状态表示的含义可得
f[1][0] = 0;
// 状态计算
for (int i = 2; i < 1 << n; i++)
for (int j = 0; j < n; j++)
// 判断路径的状态为i时,是否包括第j个结点
if (i >> j & 1)
for (int k = 0; k < n; k++)
// 判断路径的状态为i除去第j个结点后的路径的状态,是否包括第k个结点
if ((i - (1 << j)) >> k & 1)
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
// 输出结果
printf("%d\n", f[(1 << n) - 1][n - 1]);
return 0;
}
树形DP
没有上司的舞会
- 状态表示
- f[u,0](f[u,0]表示,在,由,从以u结点为根结点的子树中选择并且不选择u结点的每种选择的快乐指数总和,组成的集合,中,的最大值)
- f[u,1](f[u,1]表示,在,由,从以u结点为根结点的子树中选择并且选择u结点的每种选择的快乐指数总和,组成的集合,中,的最大值)
- 状态计算
- f[u,0]=max(f[k1,0],f[k1,1])+max(f[k2,0],f[k2,1])+…+max(f[kn,0],f[kn,1])(其中k1,k2,…,kn为u结点的子结点)
- f[u,1]=f[k1,0]+f[k2,0]+…+f[kn,0](其中k1,k2,…,kn为u结点的子结点)
没有上司的舞会
#include <iostream>
#include <cstring>
using namespace std;
const int N = 6e3 + 10;
const int M = 6e3 + 10;
int h[N];// 存储每个节点对应的单链表的表头所指向的idx
int e[M];// 存储第idx条边所指向的节点
int ne[M];// 存储第idx条边所指向的节点所指向的idx
int idx;// 表示当前用到第idx条边
int happy[N];
bool have_father[N];
int f[N][2];// 状态表示
void add(int a, int b)// 插入一条a结点指向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])
{
int j = e[i];
dfs(j);// 递归处理
// 状态计算
f[u][0] += max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &happy[i]);// 输入每个结点的快乐指数
// 初始化每个结点对应的单链表的表头所指向的idx为-1,表示每个结点与其它结点都不直接相连
memset(h, -1, sizeof h);
for (int i = 0; i <= n - 2; i++)// 在n个结点的树中,最多有n-1条边,所以是循环n-1次
{
int a, b;
scanf("%d %d", &a, &b);
add(b, a);// b结点是a结点的父结点
have_father[a] = true;// 标记a结点有父结点
}
int root = 1;
// 找到根结点
while (have_father[root])
root++;
// 从根结点开始进行,状态计算
dfs(root);
// 输出结果
printf("%d\n", max(f[root][0], f[root][1]));
return 0;
}
记忆化搜索
滑雪
- 状态表示:f[i,j](f[i,j]表示,在,由,从(i,j)这个结点开始滑的每条路径的路径长度,组成的集合,中,的最大值)
- 状态计算:f[i,j]=max(f[i-1,j]+1,f[i+1,j]+1,f[i,j+1]+1,f[i,j-1]+1)
滑雪
#include <iostream>
#include <cstring>
using namespace std;
const int N = 310;
int h[N][N];
int f[N][N];// 状态表示
int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0};// 使用偏移量,来模拟往上、下、左、右移动的四个方向
int r, c;
int dp(int x, int y)
{
if (f[x][y] != -1)
// 如果当前状态已经计算过,则直接返回结果
return f[x][y];
f[x][y] = 1;// 路径长度至少为1
for (int i = 0; i <= 3; i++)
{
int a = x + dx[i];
int b = y + dy[i];
// 状态计算
if (a >= 1 && a <= r && b >= 1 && b <= c && h[a][b] < h[x][y])
f[x][y] = max(f[x][y], dp(a, b) + 1);
}
return f[x][y];
}
int main()
{
scanf("%d %d", &r, &c);
for (int i = 1; i <= r; i++)
for (int j = 1; j <= c; j++)
scanf("%d", &h[i][j]);
// 初始化每个状态的值为-1,视为没有计算过
memset(f, -1, sizeof f);
int res = 0;
for (int i = 1; i <= r; i++)
for (int j = 1; j <= c; j++)
res = max(res, dp(i, j));
// 输出结果
printf("%d\n", res);
return 0;
}
习题
01背包问题
- 优化版
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n;
int v[N], w[N];// 每件物品都有v(体积)和w(价值)两种属性
int m;
int f[M];
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d %d", &v[i], &w[i]);
// 状态计算:f[i,j]=max(f[i-1,j],f[i-1,j-vi]+wi)(vi表示第i件物品的体积,wi表示第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]);
printf("%d\n", f[m]);
return 0;
}
完全背包问题
- 再次优化版
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n;
int v[N], w[N];// 每件物品都有v(体积)和w(价值)两种属性
int m;
int f[M];
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d %d\n", &v[i], &w[i]);
// 状态计算:f[i,j]=max(f[i-1,j],f[i,j-vi]+wi)(vi表示第i件物品的体积,wi表示第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]);
printf("%d\n", f[m]);
return 0;
}
多重背包问题 I
#include <iostream>
using namespace std;
const int N = 110;
const int M = 110;
int n;
int v[N], w[N], s[N];// 每件物品都有v(体积)和w(价值)和s(个数)三种属性
int m;
int f[N][M];// 状态表示
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d %d %d", &v[i], &w[i], &s[i]);
/*
f[0][0]~f[0][m],根据状态表示的含义,可得这些值都为0,
由于该数组为全局变量,所以已经默认为0,无需手动赋值为0
*/
// 状态计算:f[i,j]=max(f[i-1,j-k*vi]+k*wi)(vi表示第i件物品的体积,wi表示第i件物品的价值,k表示第i件物品使用了k次)
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
for (int k = 0; k * v[i] <= j && k <= s[i]; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
printf("%d\n", f[n][m]);
return 0;
}
多重背包问题 II
#include <iostream>
using namespace std;
const int N = 12000;// 1000*log2000小于11000(这里的log2000是以2为底的)
const int M = 2010;
int n;
int v[N], w[N];// 把多重背包问题转化为了01背包问题,每件物品都有v(体积)和w(价值)两种属性
int m;
int f[M];
/*
解题思路:
1、存在n件物品
2、假设第i件物品的个数为si,
把si用如下方式表示,
si=2^0+2^1+2^2+2^3+...+2^k+c(其中,c=si-(2^(k+1)-1)),
把si个第i件物品作拆分,
拆分成2^0个第i件物品,2^1个第i件物品,2^2个第i件物品,2^3个第i件物品,...,2^k个第i件物品,c个第i件物品,
即把si个第i件物品拆分成了k+2份不同数量的第i件物品,
可得,对这k+2份不同数量的第i件物品的所有选法(每一份只有选或不选,两种选择),等于对未拆分前的si个第i件物品的所有选法,
对未拆分前的si个第i件物品的所有选法为:
选择了0个第i件物品,选择了1个第i件物品,选择了2个第i件物品,...,选择了si个第i件物品,
即可以把这k+2份不同数量的第i件物品分别视为新的一件物品,每件物品只有选或不选,两种选择
3、按照步骤2,以此类推,把剩下的n-1件物品作同样的处理
4、则原来的n件物品会变成新的若干件物品,每件物品只有选或不选,两种选择
5、即把多重背包问题转化为了01背包问题
*/
int main()
{
scanf("%d %d", &n, &m);
int cnt = 0;
// 循环n次,对n件物品分别作拆分
while (n--)
{
int a, b, c;
scanf("%d %d %d", &a, &b, &c);// 体积,价值,个数
int k = 1;
while (k <= c)
{
cnt++;
v[cnt] = k * a;
w[cnt] = k * b;
c -= k;
k *= 2;
}
if (c > 0)
{
cnt++;
v[cnt] = c * a;
w[cnt] = c * b;
}
}
// 原来的n件物品会变成新的若干件物品,每件物品只有选或不选,两种选择
n = cnt;// 把多重背包问题转化为了01背包问题
// 01背包问题的状态计算:f[i,j]=max(f[i-1,j],f[i-1,j-vi]+wi)(vi表示第i件物品的体积,wi表示第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]);
printf("%d\n", f[m]);
return 0;
}
分组背包问题
#include <iostream>
using namespace std;
const int N = 110;
const int M = 110;
int n;
int s[N];
int v[N][N], w[N][N];// 存在N组物品,每组物品有若干件物品,每件物品都有v(体积)和w(价值)两种属性
int m;
int f[M];
int main()
{
scanf("%d %d", &n, &m);
// 存在N组物品
for (int i = 1; i <= n; i++)
{
// 每组物品有若干件物品
scanf("%d", &s[i]);
// 每件物品都有v(体积)和w(价值)两种属性
for (int j = 0; j < s[i]; j++)
scanf("%d %d", &v[i][j], &w[i][j]);
}
// 状态计算:f[i,j]=max(f[i-1,j-v[i,k]]+w[i,k])(v[i,k]表示第i组物品中的第k件物品的体积,w[i,k]表示第i组物品中的第k件物品的价值,当k为0时则表示第i组物品中不选择任何一件物品)
// 状态计算的过程可以用一维数组去优化
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]);
printf("%d\n", f[m]);
return 0;
}
数字三角形
#include <iostream>
using namespace std;
const int N = 510;
const int INF = 1e9;
int n;
int a[N][N];
int f[N][N];// 状态表示
int main()
{
scanf("%d", &n);
// 初始化每个结点的数字
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
scanf("%d\n", &a[i][j]);
// 结合状态计算分析,因为存在左上方没有结点的结点和存在右上方没有结点的结点,因此要对边界作预处理
for (int i = 1; i <= n; i++)
for (int j = 0; j <= i + 1; j++)
f[i][j] = -INF;
// (1,1)这个结点为起点,根据状态表示的含义,可得f[1][1]=a[1][1]
f[1][1] = a[1][1];
// 状态计算:f[i,j]=max(f[i-1,j-1]+a[i,j],f[i-1,j]+a[i,j])(a[i,j]表示(i,j)这个结点的数字)
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 j = 1; j <= n; j++)
res = max(res, f[n][j]);
printf("%d\n", res);
return 0;
}
最长上升子序列
#include <iostream>
using namespace std;
const int N = 1010;
int n;
int a[N];
int f[N];// 状态表示
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d\n", &a[i]);
// 状态计算:f[i]=max(f[j]+1)(其中,j∈{1,2,...,i-1})
for (int i = 1; i <= n; i++)
{
f[i] = 1;// 以第i个数结尾的每个上升子序列的长度最短为1
for (int j = 1; j <= n - 1; 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;
}
最长上升子序列 II
解题思路更像贪心
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
/*
q[0]除外,
q[1]表示,在,由,a数组中上升子序列的长度为1的每个上升子序列的末尾值,组成的集合,中,的最小值,
q[2]表示,在,由,a数组中上升子序列的长度为2的每个上升子序列的末尾值,组成的集合,中,的最小值,
q[3]表示,在,由,a数组中上升子序列的长度为3的每个上升子序列的末尾值,组成的集合,中,的最小值,
...,
q[i]表示,在,由,a数组中上升子序列的长度为i的每个上升子序列的末尾值,组成的集合,中,的最小值,
且q数组一定满足:q[0]<q[1]<q[2]<q[3]<...<q[i]
*/
int q[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
int len = 0;// 记录a数组中最长上升子序列的长度
q[0] = -2e9;// 保证q数组中最左边第一个元素的值小于a数组中任意元素的值
for (int i = 0; i < n; i++)// 遍历a数组中每个元素
{
int l = 0;// 二分区间的左边界
int r = len;// 二分区间的右边界
while (l < r)// 在q数组中找到第一个小于a[i]的元素的下标
{
int mid = (l + r + 1) / 2;
/*
当l与r差1时,(l+r)/2的结果一定会等于l,
此时会导致下一行代码执行后,发生死循环,
因此上一行代码必须改为(l+r+1)/2
*/
if (q[mid] < a[i])
l = mid;
else
r = mid - 1;
}
len = max(len, r + 1);
// q[r]为q数组中第一个小于a[i]的元素,则a[i]要放到q数组中的下标为r+1的位置上,即q[r+1]=a[i]
q[r + 1] = a[i];
}
printf("%d\n", len);
return 0;
}
最长公共子序列
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n, m;
char a[N], b[M];
int f[N][M];
int main()
{
scanf("%d %d", &n, &m);
scanf("%s %s", a + 1, b + 1);
// 状态计算:f[i,j]=max(f[i-1,j-1],f[i-1,j],f[i,j-1],f[i-1,j-1]+1)=max(f[i-1,j],f[i,j-1],f[i-1,j-1]+1)(划分集合时,如果集合的属性为最大值,则各个子集合间允许存在交集)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
f[i][j] = f[i - 1][j];
f[i][j] = max(f[i][j], f[i][j - 1]);
if (a[i] == b[j])// 公共子序列
f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
printf("%d\n", f[n][m]);
return 0;
}
最短编辑距离
最短编辑距离
- 状态表示:f[i,j](f[i,j]表示,在,由,将a[1~i]变成b[1~j]的每个操作方式的操作次数,组成的集合,中,的最小值)
- 状态计算:f[i,j]=min(f[i-1,j]+1,f[i,j-1]+1,f[i-1,j-1]+1,f[i-1,j-1])
#include <iostream>
using namespace std;
const int N = 1010;
const int M = 1010;
int n, m;
char a[N], b[M];
int f[N][M];// 状态表示
int main()
{
scanf("%d %s", &n, a + 1);
scanf("%d %s", &m, b + 1);
// 根据状态表示的含义可得
for (int i = 0; i <= n; i++)
f[i][0] = i;
for (int j = 0; j <= m; j++)
f[0][j] = j;
// 状态计算:f[i,j]=min(f[i-1,j]+1,f[i,j-1]+1,f[i-1,j-1]+1,f[i-1,j-1])
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{
f[i][j] = f[i - 1][j] + 1;
f[i][j] = min(f[i][j], f[i][j - 1] + 1);
if (a[i] != b[j])
f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
else
f[i][j] = min(f[i][j], f[i - 1][j - 1]);
}
printf("%d\n", f[n][m]);
return 0;
}
编辑距离
编辑距离
- 状态表示:f[i,j](f[i,j]表示,在,由,将a[1~i]变成b[1~j]的每个操作方式的操作次数,组成的集合,中,的最小值)
- 状态计算:f[i,j]=min(f[i-1,j]+1,f[i,j-1]+1,f[i-1,j-1]+1,f[i-1,j-1])
#include <iostream>
#include <cstring>
using namespace std;
const int A = 15;
const int B = 15;
const int TOTAL = 1010;
int f[A][B];// 状态表示
char a[TOTAL][A];
int edit_distance(char a[], char b[])
{
// 从字符数组的哪个位置开始读入字符,就从哪个位置开始统计并返回字符数组的长度
int la = strlen(a + 1);
int rb = strlen(b + 1);
// 根据状态表示的含义可得
for (int i = 0; i <= la; i++)
f[i][0] = i;
for (int j = 0; j <= rb; j++)
f[0][j] = j;
// 状态计算:f[i,j]=min(f[i-1,j]+1,f[i,j-1]+1,f[i-1,j-1]+1,f[i-1,j-1])
for (int i = 1; i <= la; i++)
for (int j = 1; j <= rb; j++)
{
f[i][j] = f[i - 1][j] + 1;
f[i][j] = min(f[i][j], f[i][j - 1] + 1);
if (a[i] != b[j])
f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
else
f[i][j] = min(f[i][j], f[i - 1][j - 1]);
}
return f[la][rb];
}
int main()
{
int n, m;
scanf("%d %d", &n, &m);
for (int i = 0; i < n; i++)
scanf("%s", a[i] + 1);
while (m--)// 询问m次
{
char b[B];
int limit;
// 每次询问给出一个字符串和一个操作次数上限
scanf("%s %d", b + 1, &limit);
int res = 0;
for (int i = 0; i < n; i++)
// 判断a[i]是否能在给出的操作次数上限内经过操作变成给出的b
if (edit_distance(a[i], b) <= limit)
res++;
printf("%d\n", res);
}
return 0;
}
石子合并
#include <iostream>
using namespace std;
const int N = 310;
int n;
int s[N];// 前缀和数组
int f[N][N];// 状态表示
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", &s[i]);
// 前缀和
s[i] += s[i - 1];
}
// 按区间长度从小到大进行遍历
for (int len = 2; len <= n; len++)// 区间长度从2开始
{
// 遍历当前区间长度的左边界
for (int t = 1; t + len - 1 <= n; t++)
{
int l = t;// 左边界
int r = t + len - 1;// 右边界
f[l][r] = 1e9;
/*
状态计算:f[i,j]=min(f[i,k]+f[k+1,j]+s[j]-s[i-1])
(s[j]表示第1堆石子到第j堆石子的总重量,
s[i-1]表示第1堆石子到第i-1堆石子的总重量,
s[j]-s[i-1]表示第i堆石子到第j堆石子的总重量,
k∈{i,i+1,i+2,...,j-1})
*/
for (int k = l; k <= r - 1; k++)
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;
}
整数划分
- 第二种解法
#include <iostream>
using namespace std;
const int MOD = 1e9 + 7;
const int N = 1010;
int f[N][N];// 状态表示
int main()
{
int n;
scanf("%d", &n);
// 0被恰好表示成0个正整数的和,的组合数为1
f[0][0] = 1;
/*
解题思路:
状态表示:f[i,j](f[i,j]表示,正整数i被恰好表示成j个正整数的和,的组合数)
状态计算:f[i,j]=f[i-1][j-1]+f[i-j][j](其中j<=i,因为正整数i最多只能被表示成i个正整数的和,即i个1的和)
*/
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)// j<=i,因为正整数i最多只能被表示成i个正整数的和,即i个1的和
f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % MOD;
int res = 0;
for (int j = 1; j <= n; j++)
res = (res + f[n][j]) % MOD;
printf("%d\n", res);
return 0;
}
计数问题
#include <iostream>
#include <vector>
using namespace std;
// 返回num数组中下标从l到r(从高位到低位)的每一位数字所构成的数值
int get(vector<int> num, int l, int r)
{
int res = 0;
for (int i = l; i >= r; i--)
res = res * 10 + num[i];
return res;
}
// 返回10的n次方的结果
int power10(int n)
{
int res = 1;
while (n--)
res *= 10;
return res;
}
/*
这个count()函数的思路:
1、假设n的数值为abcdefg
2、要求在1~abcdefg中,数字x出现的次数,
则只需要求出数字x在每一位上出现的次数,
然后把这些次数相加求和即可
3、举个例子,abcxefg,要求数字x在当前位上出现的次数,
分情况讨论(当数字x在最高位时则不存在第1种情况。当数字x为0时则第2种情况不能从最高位开始):
1、当高位的前三位取值为000~abc-1时,
低位的后三位可取值的范围为000~999,
则数字x在当前位上可出现的次数为abc*1000。
如果数字x为0,则高位的前三位取值不能为000
2、当高位的前三位取值为abc时,
分情况讨论:
1、当d等于数字x时,
低位的后三位可取值的范围为000~efg,
则数字x在当前位上可出现的次数为efg+1
2、当d大于数字x时,
低位的后三位可取值的范围为000~999,
则数字x在当前位上可出现的次数为1000
把这两种情况的数字x在当前位上可出现的次数相加求和,
即为数字x在当前位上出现的次数
*/
int count(int n, int x)// 返回在1~n中,数字x出现的次数
{
vector<int> num;
// 先把n中从低位到高位的每一位数字依次放入到num数组中
while (n)
{
num.push_back(n % 10);
n /= 10;
}
int res = 0;
int len = num.size();
// 求出数字x在每一位上出现的次数
for (int i = len - 1 - !x; i >= 0; i--)// 当数字x为0时则第2种情况不能从最高位开始
{
// 第1种情况
if (i <= len - 2)// 当数字x在最高位时则不存在第1种情况
{
res += get(num, len - 1, i + 1) * power10(i);
if (!x)// 如果数字x为0,则高位的取值不能为0
res -= power10(i);
}
// 第2种情况
if (num[i] == x)
res += get(num, i - 1, 0) + 1;
if (num[i] > x)
res += power10(i);
}
return res;
}
int main()
{
int a, b;
while (cin >> a >> b, a || b)// 当a和b都为0时,则结束循环
{
if (a > b)
swap(a, b);
for (int i = 0; i <= 9; i++)
/*
count(b,i)表示在1~b中,数字i出现的次数,
count(a,i)表示在1~a中,数字i出现的次数,
count(b,i)-count(a-1,i)表示在a~b中,数字i出现的次数
*/
printf("%d ", count(b, i) - count(a - 1, i));
puts("");
}
return 0;
}
蒙德里安的梦想
#include <iostream>
#include <cstring>
using namespace std;
const int ROW = 15, COL = 15;
const int ROW_STATE = 1 << ROW;
long long f[COL][ROW_STATE];// 状态表示
bool st[ROW_STATE];
int main()
{
int r, c;
while (cin >> r >> c, r || c)// 当r和c都为0时,则结束循环
{
// 预处理每个行的状态所对应的二进制表示的数,是否存在连续奇数个0
for (int i = 0; i < 1 << r; i++)
{
st[i] = true;
int cnt = 0;
// 枚举i所对应的二进制表示的数的每一位
for (int j = 0; j < r; j++)
// 每次遇到一个1,就需要判断上一次统计的连续的0的个数
if (i >> j & 1)
{
if (cnt & 1)// 判断cnt是否为奇数
{
st[i] = false;// 存在连续奇数个0
break;
}
cnt = 0;
}
else
cnt++;
if (cnt & 1)// 判断cnt是否为奇数
st[i] = false;// 存在连续奇数个0
}
memset(f, 0, sizeof f);
// 根据状态表示的含义可得
f[0][0] = 1;
// 状态计算
for (int i = 1; i <= c; i++)
for (int j = 0; j < 1 << r; j++)
for (int k = 0; k < 1 << r; k++)
if ((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
// 输出结果
printf("%lld\n", f[c][0]);
}
return 0;
}
最短Hamilton路径
#include <iostream>
#include <cstring>
using namespace std;
const int N = 21;// 注意,N的上限为21,因为此题有空间限制
const int PATH_STATE = 1 << N;
int f[PATH_STATE][N];// 状态表示
int w[N][N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
scanf("%d", &w[i][j]);
memset(f, 0x3f, sizeof f);
// 根据状态表示的含义可得
f[1][0] = 0;
// 状态计算
for (int i = 2; i < 1 << n; i++)
for (int j = 0; j < n; j++)
// 判断路径的状态为i时,是否包括第j个结点
if (i >> j & 1)
for (int k = 0; k < n; k++)
// 判断路径的状态为i除去第j个结点后的路径的状态,是否包括第k个结点
if ((i - (1 << j)) >> k & 1)
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
// 输出结果
printf("%d\n", f[(1 << n) - 1][n - 1]);
return 0;
}
没有上司的舞会
#include <iostream>
#include <cstring>
using namespace std;
const int N = 6e3 + 10;
const int M = 6e3 + 10;
int h[N];// 存储每个节点对应的单链表的表头所指向的idx
int e[M];// 存储第idx条边所指向的节点
int ne[M];// 存储第idx条边所指向的节点所指向的idx
int idx;// 表示当前用到第idx条边
int happy[N];
bool have_father[N];
int f[N][2];// 状态表示
void add(int a, int b)// 插入一条a结点指向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])
{
int j = e[i];
dfs(j);// 递归处理
// 状态计算
f[u][0] += max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &happy[i]);// 输入每个结点的快乐指数
// 初始化每个结点对应的单链表的表头所指向的idx为-1,表示每个结点与其它结点都不直接相连
memset(h, -1, sizeof h);
for (int i = 0; i <= n - 2; i++)// 在n个结点的树中,最多有n-1条边,所以是循环n-1次
{
int a, b;
scanf("%d %d", &a, &b);
add(b, a);// b结点是a结点的父结点
have_father[a] = true;// 标记a结点有父结点
}
int root = 1;
// 找到根结点
while (have_father[root])
root++;
// 从根结点开始进行,状态计算
dfs(root);
// 输出结果
printf("%d\n", max(f[root][0], f[root][1]));
return 0;
}
滑雪
#include <iostream>
#include <cstring>
using namespace std;
const int N = 310;
int h[N][N];
int f[N][N];// 状态表示
int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0};// 使用偏移量,来模拟往上、下、左、右移动的四个方向
int r, c;
int dp(int x, int y)
{
if (f[x][y] != -1)
// 如果当前状态已经计算过,则直接返回结果
return f[x][y];
f[x][y] = 1;// 路径长度至少为1
for (int i = 0; i <= 3; i++)
{
int a = x + dx[i];
int b = y + dy[i];
// 状态计算
if (a >= 1 && a <= r && b >= 1 && b <= c && h[a][b] < h[x][y])
f[x][y] = max(f[x][y], dp(a, b) + 1);
}
return f[x][y];
}
int main()
{
scanf("%d %d", &r, &c);
for (int i = 1; i <= r; i++)
for (int j = 1; j <= c; j++)
scanf("%d", &h[i][j]);
// 初始化每个状态的值为-1,视为没有计算过
memset(f, -1, sizeof f);
int res = 0;
for (int i = 1; i <= r; i++)
for (int j = 1; j <= c; j++)
res = max(res, dp(i, j));
// 输出结果
printf("%d\n", res);
return 0;
}
区间问题
区间选点
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
struct Range
{
int l, r;
// 重载小于号
bool operator< (const Range& t) const
{
return r < t.r;
}
}range[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int l, r;
scanf("%d %d", &l, &r);
range[i] = {l, r};
}
// 将每个区间按右端点从小到大进行排序
sort(range, range + n);
int res = 0;
int ed = -2e9;
// 遍历按右端点从小到大排好序的每个区间
for (int i = 0; i < n; i++)
// 判断下一个区间的左端点是否大于上一个区间的右端点
if (range[i].l > ed)
{
res++;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
最大不相交区间数量
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
struct Range
{
int l, r;
// 重载小于号
bool operator< (const Range& t) const
{
return r < t.r;
}
}range[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int l, r;
scanf("%d %d", &l, &r);
range[i] = {l, r};
}
// 将每个区间按右端点从小到大进行排序
sort(range, range + n);
int res = 0;
int ed = -2e9;
// 遍历按右端点从小到大排好序的每个区间
for (int i = 0; i < n; i++)
// 判断下一个区间的左端点是否大于上一个区间的右端点
if (range[i].l > ed)
{
res++;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
区间分组
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
struct Range
{
int l, r;
// 重载小于号
bool operator< (const Range& t) const
{
return l < t.l;
}
}range[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int l, r;
scanf("%d %d", &l, &r);
range[i] = {l, r};
}
// 将每个区间按左端点从小到大进行排序
sort(range, range + n);
priority_queue<int, vector<int>, greater<int>> heap;// 小根堆
// 遍历按左端点从小到大排好序的每个区间
for (int i = 0; i < n; i++)
{
Range t = range[i];
if (heap.empty() || heap.top() >= t.l)
// 开一个新的组
heap.push(t.r);// 每个组存的都是,在,由,当前组的每个区间的右端点,组成的集合,中,的最大值
else
{
heap.pop();
heap.push(t.r);
}
}
printf("%d\n", heap.size());
return 0;
}
区间覆盖
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
struct Range
{
int l, r;
// 重载小于号
bool operator< (const Range& t) const
{
return l < t.l;
}
}range[N];
int main()
{
int st, ed;// 目标区间的左端点和右端点
scanf("%d %d", &st, &ed);
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int l, r;
scanf("%d %d", &l, &r);
range[i] = {l, r};
}
// 将每个区间按左端点从小到大进行排序
sort(range, range + n);
bool flag = false;
int res = 0;
// 遍历按左端点从小到大排好序的每个区间
for (int i = 0; i < n; i++)
{
int j = i;
int r = -2e9;
// 在区间的左端点小于等于目标区间的左端点的每个区间中,找到区间的右端点最大的一个区间
while (j < n && range[j].l <= st)
{
r = max(r, range[j].r);
j++;
}
if (r < st)
// 无法完全覆盖目标区间
break;
res++;
if (r >= ed)
{
flag = true;// 可以完全覆盖目标区间
break;
}
st = r;// 更新目标区间的左端点
i = j - 1;
}
printf("%d\n", flag ? res : -1);
return 0;
}
Huffman树
#include <iostream>
#include <queue>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
// 小根堆
priority_queue<int, vector<int>, greater<int>> heap;
while (n--)
{
int x;
scanf("%d", &x);
heap.push(x);
}
int res = 0;
// 每次从中选择果子数量最小的两种果子进行合并
while (heap.size() > 1)
{
int a = heap.top(); heap.pop();
int b = heap.top(); heap.pop();
res += a + b;
heap.push(a + b);
}
// 输出结果
printf("%d\n", res);
return 0;
}
排序不等式
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int t[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &t[i]);
// 按打水时间从短到长进行排序
sort(t, t + n);
long long res = 0;
for (int i = 0; i < n; i++)
res += t[i] * (n - i - 1);
printf("%lld\n", res);
return 0;
}
绝对值不等式
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
// 按商店位置从左到右进行排序
sort(a, a + n);
int res = 0;
// 货仓位置选择建在所有商店位置的中间
for (int i = 0; i < n; i++)
res += abs(a[i] - a[n / 2]);
printf("%d\n", res);
return 0;
}
推公式
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5e5 + 10;
pair<int, int> cow[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int w, s;
scanf("%d %d", &w, &s);
cow[i] = {w + s, s};
}
// 按每头牛的重量与强壮值之和从小到大进行排序
sort(cow, cow + n);
int res = -2e9;
int sum = 0;
for (int i = 0; i < n; i++)
{
int w = cow[i].first - cow[i].second;
res = max(res, sum - cow[i].second);
sum += w;
}
// 输出结果
printf("%d\n", res);
return 0;
}
习题
区间选点
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
struct Range
{
int l, r;
// 重载小于号
bool operator< (const Range& t) const
{
return r < t.r;
}
}range[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int l, r;
scanf("%d %d", &l, &r);
range[i] = {l, r};
}
// 将每个区间按右端点从小到大进行排序
sort(range, range + n);
int res = 0;
int ed = -2e9;
// 遍历按右端点从小到大排好序的每个区间
for (int i = 0; i < n; i++)
// 判断下一个区间的左端点是否大于上一个区间的右端点
if (range[i].l > ed)
{
res++;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
最大不相交区间数量
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
struct Range
{
int l, r;
// 重载小于号
bool operator< (const Range& t) const
{
return r < t.r;
}
}range[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int l, r;
scanf("%d %d", &l, &r);
range[i] = {l, r};
}
// 将每个区间按右端点从小到大进行排序
sort(range, range + n);
int res = 0;
int ed = -2e9;
// 遍历按右端点从小到大排好序的每个区间
for (int i = 0; i < n; i++)
// 判断下一个区间的左端点是否大于上一个区间的右端点
if (range[i].l > ed)
{
res++;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
区间分组
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
struct Range
{
int l, r;
// 重载小于号
bool operator< (const Range& t) const
{
return l < t.l;
}
}range[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int l, r;
scanf("%d %d", &l, &r);
range[i] = {l, r};
}
// 将每个区间按左端点从小到大进行排序
sort(range, range + n);
priority_queue<int, vector<int>, greater<int>> heap;// 小根堆
// 遍历按左端点从小到大排好序的每个区间
for (int i = 0; i < n; i++)
{
Range t = range[i];
if (heap.empty() || heap.top() >= t.l)
// 开一个新的组
heap.push(t.r);// 每个组存的都是,在,由,当前组的每个区间的右端点,组成的集合,中,的最大值
else
{
heap.pop();
heap.push(t.r);
}
}
printf("%d\n", heap.size());
return 0;
}
区间覆盖
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
struct Range
{
int l, r;
// 重载小于号
bool operator< (const Range& t) const
{
return l < t.l;
}
}range[N];
int main()
{
int st, ed;// 目标区间的左端点和右端点
scanf("%d %d", &st, &ed);
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int l, r;
scanf("%d %d", &l, &r);
range[i] = {l, r};
}
// 将每个区间按左端点从小到大进行排序
sort(range, range + n);
bool flag = false;
int res = 0;
// 遍历按左端点从小到大排好序的每个区间
for (int i = 0; i < n; i++)
{
int j = i;
int r = -2e9;
// 在区间的左端点小于等于目标区间的左端点的每个区间中,找到区间的右端点最大的一个区间
while (j < n && range[j].l <= st)
{
r = max(r, range[j].r);
j++;
}
if (r < st)
// 无法完全覆盖目标区间
break;
res++;
if (r >= ed)
{
flag = true;// 可以完全覆盖目标区间
break;
}
st = r;// 更新目标区间的左端点
i = j - 1;
}
printf("%d\n", flag ? res : -1);
return 0;
}
合并果子
#include <iostream>
#include <queue>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
// 小根堆
priority_queue<int, vector<int>, greater<int>> heap;
while (n--)
{
int x;
scanf("%d", &x);
heap.push(x);
}
int res = 0;
// 每次从中选择果子数量最小的两种果子进行合并
while (heap.size() > 1)
{
int a = heap.top(); heap.pop();
int b = heap.top(); heap.pop();
res += a + b;
heap.push(a + b);
}
// 输出结果
printf("%d\n", res);
return 0;
}
排队打水
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int t[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &t[i]);
// 按打水时间从短到长进行排序
sort(t, t + n);
long long res = 0;
for (int i = 0; i < n; i++)
res += t[i] * (n - i - 1);
printf("%lld\n", res);
return 0;
}
货仓选址
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
// 按商店位置从左到右进行排序
sort(a, a + n);
int res = 0;
// 货仓位置选择建在所有商店位置的中间
for (int i = 0; i < n; i++)
res += abs(a[i] - a[n / 2]);
printf("%d\n", res);
return 0;
}
耍杂技的牛
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5e5 + 10;
pair<int, int> cow[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int w, s;
scanf("%d %d", &w, &s);
cow[i] = {w + s, s};
}
// 按每头牛的重量与强壮值之和从小到大进行排序
sort(cow, cow + n);
int res = -2e9;
int sum = 0;
for (int i = 0; i < n; i++)
{
int w = cow[i].first - cow[i].second;
res = max(res, sum - cow[i].second);
sum += w;
}
// 输出结果
printf("%d\n", res);
return 0;
}
时空复杂度分析
由数据范围反推算法复杂度以及算法内容

单位转换
bit ------除以8 ------> Byte
Byte ------除以1024------> KByte
KByte ------除以1024------> MByte
MByte ------除以1024------> GByte
&spm=1001.2101.3001.5002&articleId=135111026&d=1&t=3&u=c38099d6bcaa4824aa6f4e65ce2761fb)
412

被折叠的 条评论
为什么被折叠?



