注意,状态转移方程应该是 f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) f[ i ][ j ] = max( f[ i - 1 ][ j ], f[ i - 1 ][ j - v[ i ]] + w[ i ]) f[i][j]=max(f[i−1][j],f[i−1][j−v[i]]+w[i]),但是由于前面写了个 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[ i ][ j ] = f[ i - 1 ][ j ] f[i][j]=f[i−1][j],因此就成了代码中的那个样子。
//v是物体的体积,w是物体的价值
int v[maxn], w[maxn], f[maxn][maxn];
int N, V;
int main() {
scanf("%d%d", &N, &V);
for (int i = 1; i <= N; i++) scanf("%d%d", &v[i], &w[i]);
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
f[i][j] = f[i - 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
printf("%d\n", f[N][V]);
return 0;
}
那么,这个怎么改成一维呢?我们发现,在计算 f [ i ] [ j ] f[ i ][ j ] f[i][j]的时候,用到的之后上面那一层 f [ i − 1 ] [ j ] 和 f [ i − 1 ] [ j − v [ i ] ] , v [ i ] < = j f[ i - 1 ][ j ]和f[ i - 1][ j - v[ i ]],\ v[ i ] <= j f[i−1][j]和f[i−1][j−v[i]], v[i]<=j,因此可以这么改:
for (int i = 1; i <= N; i++) {
for (int j = V; j >= v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
printf("%d\n", f[V]);
#include
#include
using namespace std;
const int maxn = 1010;
int f[maxn][maxn], v[maxn], w[maxn], N, V;
int main() {
scanf("%d%d", &N, &V);
for (int i = 1; i <= N; i++) scanf("%d%d", &v[i], &w[i]);
for (int i = N; i >= 1; i--) {
//注意这里从小到大循环并不是完全背包。因为这个是二维数组,不是一维数组。
for (int j = 0; j <= V; j++) {
f[i][j] = f[i + 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
}
}
int j = V;
for (int i = 1; i <= N; i++) {
if (j >= v[i] && f[i + 1][j - v[i]] + w[i] == f[i][j]) {
printf("%d ", i);
j -= v[i];
}
}
return 0;
}
朴素写法:
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; 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][V]);
改进一下:
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; 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]);
}
}
改为1维滚动数组
for (int i = 1; i <= N; i++) {
for (int j = v[i]; j <= V; j++){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
题意:第 i i i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi。
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; 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]);
}
}
}
改进写法:
打包。我们用一组
2
2
2 的
k
−
1
k - 1
k−1 次幂的数字,可以凑出
1
∼
2
k
−
1
1\sim 2^{k - 1}
1∼2k−1 的任何一个数字。那么我们可以用这么一个性质,将这些物品打包。而凑数字的过程又和
01
01
01 背包的选的过程是一模一样的。因此就转化成了
01
01
01 背包问题。
注意这次要用滚动数组,不然会爆内存。那个数组的大小要开到大于
v
∗
l
o
g
S
v * logS
v∗logS.
#include
#include
using namespace std;
const int maxn = 25000, maxm = 2010;
int v[maxn], w[maxn], f[maxm];
int N, V;
int main() {
scanf("%d%d", &N, &V);
int cnt = 0;
for (int i = 1; i <= N; i++) {
int a, b, s;
scanf("%d%d%d", &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 = V; j >= v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
printf("%d\n", f[V]);
return 0;
}

#include
using namespace std;
const int N = 1010, M = 200010;
int f[M], g[M], q[M];
int n, m;
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
{
int v, w, s;
scanf("%d%d%d", &v, &w, &s);
//g存的是上一层的f的值
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 - q[hh] > s * v) hh++;
//借助滑动窗口最大值更新答案
if(hh <= tt) f[k] = max(g[k], g[q[hh]] + (k - q[hh]) / v * w);
//单调队列删除冗余元素
while(hh <= tt && (k - q[tt]) / v * w + g[q[tt]] <= g[k]) tt--;
q[++tt] = k;
}
}
}
printf("%d\n", f[m]);
return 0;
}
#include
#include
using namespace std;
const int maxn = 110, maxv = 110;
int v[maxn][maxn], w[maxn][maxn], f[maxv], s[maxn];
int N, V;
int main() {
scanf("%d%d", &N, &V);
for (int i = 1; i <= N; i++) {
scanf("%d", &s[i]);
for (int j = 0; j < s[i]; j++) {
scanf("%d%d", &v[i][j], &w[i][j]);
}
}
//其实这个和01背包的思路差不多,但是一定不要把第二重和第三重循环反过来。如果反过来的话,不能保证每组只选一个物品。
for (int i = 1; i <= N; i++) {
for (int j = V; j >= 0; j--) {
for (int k = 0; k < s[i]; k++) {
if (j >= v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
printf("%d\n", f[V]);
return 0;
}
#include
#include
using namespace std;
const int maxn = 1010, maxm = 510, maxk = 110;
int v1[maxk], v2[maxk], f[maxn][maxm], V1, V2, N;
int main() {
scanf("%d%d%d", &V1, &V2, &N);
for (int i = 1; i <= N; i++) scanf("%d%d", &v1[i], &v2[i]);
for (int i = 1; i <= N; i++) {
for (int j = V1; j >= v1[i]; j--) {
for (int k = V2; k >= v2[i]; k--) {
f[j][k] = max(f[j][k], f[j - v1[i]][k - v2[i]] + 1);
}
}
}
int ans1 = f[V1][V2 - 1], ans2 = V2;
for (int j = 0; j <= V2 - 1; j++) {
if (f[V1][j] == ans1) {
ans2 = j; break;
}
}
printf("%d %d\n", ans1, V2 - ans2);
return 0;
}
#include
#include
#include
using namespace std;
const int maxn = 25, maxm = 85;
int V1, V2, N, f[maxn][maxm];
int main() {
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
scanf("%d%d%d", &V1, &V2, &N);
for (int i = 0; i < N; i++) {
int v1, v2, w;
scanf("%d%d%d", &v1, &v2, &w);
for (int j = V1; j >= 0; j--) {
for (int k = V2; k >= 0; k--) {
f[j][k] = min(f[j][k], f[max(0, j - v1)][max(0, k - v2)] + w);
}
}
}
printf("%d\n", f[V1][V2]);
return 0;
}
#include
#include
using namespace std;
const int maxv = 1010;
int f[maxv], N, V;
int main() {
scanf("%d%d", &N, &V);
for (int i = 1; i <= N; i++) {
int v, w, s;
scanf("%d%d%d", &v, &w, &s);
if (s == 0) {
for (int j = v; j <= V; j++) f[j] = max(f[j], f[j - v] + w);
}
else {
if (s == -1) s = 1;
for (int k = 1; k <= s; s -= k, k *= 2) {
for (int j = V; j >= v * k; j--) f[j] = max(f[j], f[j - k * v] + k * w);
}
if (s > 0) {
for (int j = V; j >= v * s; j--) f[j] = max(f[j], f[j - s * v] + s * w);
}
}
}
printf("%d\n", f[V]);
return 0;
}
#include
using namespace std;
const int N = 110, V = 110;
int f[N][V], v[N], w[N];
int h[N], e[N], ne[N], idx;
int n, m;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
for(int i = h[u]; i != -1; i = ne[i])
{
int son = e[i];
dfs(son);
//分组背包,第一层循环背包体积,第二层循环每组的物品,即子树占用的体积。
for(int j = m - v[u]; j >= 0; j--)
{
for(int k = j; k >= 0; k--)
{
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
}
}
//把根节点放进去
//注意,这里不能取max,因为这个模拟的是选子节点必须选父结点的过程。
//如果取max的话就意味着有些方案取子结点但没取父结点,结果可能会偏大。
for(int j = m; j >= v[u]; j--)
{
f[u][j] = f[u][j - v[u]] + w[u];
}
//放不进根节点的方案,全部置为0.
for(int j = 0; j < v[u]; j++) f[u][j] = 0;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
int root;
for(int i = 1; i <= n; i++)
{
int p;
scanf("%d%d%d", &v[i], &w[i], &p);
if(p == -1) root = i;
else add(p, i);
}
dfs(root);
printf("%d\n", f[root][m]);
}
优化至 O ( n 2 ) O(n^2) O(n2)
//林靖轩的写法
int dfs(int u)
{
f[u][v[u]] = w[u], f[u][0] = 0;
sz[u] = v[u];
for(int i = h[u]; i != -1; i = ne[i])
{
int son = e[i];
sz[u] += dfs(son);
//分组背包,第一层循环背包体积,第二层循环每组的物品,即子树占用的体积。
for(int j = min(sz[u], m); j >= v[u]; j--)
{
for(int k = j - v[u]; k >= 0; k--)
{
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
}
}
//放不进根节点的方案,全部置为0.
for(int j = 0; j < v[u]; j++) f[u][j] = 0;
return sz[u];
}
#include
#include
using namespace std;
const int maxm = 10010;
int N, M, f[maxm];
int main() {
scanf("%d%d", &N, &M);
f[0] = 1;
for (int i = 0; i < N; i++) {
int a;
scanf("%d", &a);
for (int j = M; j >= a; j--) {
f[j] += f[j - a];
}
}
printf("%d\n", f[M]);
return 0;
}
二维:
#include
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N][N], v[N], w[N], cnt[N][N];
int n, m;
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
{
scanf("%d%d", &v[i], &w[i]);
}
for(int i = 0; i <= n; i++) cnt[i][0] = 1;
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j], cnt[i][j] = cnt[i - 1][j];
if(j >= v[i])
{
if(f[i][j] == f[i - 1][j - v[i]] + w[i])
{
cnt[i][j] = (cnt[i][j] + cnt[i - 1][j - v[i]]) % mod;
}
else if(f[i][j] < f[i - 1][j - v[i]] + w[i])
{
cnt[i][j] = cnt[i - 1][j - v[i]];
f[i][j] = f[i - 1][j - v[i]] + w[i];
}
}
}
}
int res = 0;
for(int j = 0; j <= m; j++)
{
if(f[n][j] == f[n][m]) res = (res + cnt[n][j]) % mod;
}
printf("%d\n", res);
}
压到1维滚动数组:
#include
#include
#include
using namespace std;
const int maxn = 1010, mod = 1000000007;
int N, V, f[maxn], g[maxn];
int main() {
scanf("%d%d", &N, &V);
memset(f, -0x3f, sizeof f);
f[0] = 0, g[0] = 1;
for (int i = 1; i <= N; i++) {
int v, w;
scanf("%d%d", &v, &w);
for (int j = V; j >= v; j--) {
int maxv = max(f[j], f[j - v] + w);
int cnt = 0;
//在决策是否选择第i个物品时,需要看maxv是从哪个值过去的。
if (maxv == f[j]) cnt += g[j];
if (maxv == f[j - v] + w) cnt += g[j - v];
cnt %= mod;
g[j] = cnt, f[j] = maxv;
}
}
int res = 0, ans = 0;
for (int i = 0; i <= V; i++) res = max(res, f[i]);
for (int i = 0; i <= V; i++) {
if (f[i] == res) ans = (ans + g[i]) % mod;
}
printf("%d\n", ans);
return 0;
}
#include
#include
using namespace std;
const int maxm = 1010;
int N, f[maxm], a[] = { 10, 20, 50, 100 };
int main() {
scanf("%d", &N);
f[0] = 1;
for (int i = 0; i < 4; i++) {
for (int j = a[i]; j <= N; j++) {
f[j] += f[j - a[i]];
}
}
printf("%d\n", f[N]);
return 0;
}

f ( i , j ) f(i,j) f(i,j):在前 i i i 个物品中选体积为 j j j 的方案数。 f ( 0 , 0 ) = 0. f ( i , j ) = f ( i − 1 , j ) + f ( i , j − i ) f(0, 0) = 0.\ f(i, j) = f(i- 1, j) + f(i, j - i) f(0,0)=0. f(i,j)=f(i−1,j)+f(i,j−i).
压缩成一维:
#include
using namespace std;
const int maxn = 1010, mod = 1000000007;
int f[maxn], N;
int main() {
scanf("%d", &N);
f[0] = 1;
for (int i = 1; i <= N; i++) {
for (int j = i; j <= N; j++) {
f[j] = (f[j] + f[j - i]) % mod;
}
}
printf("%d\n", f[N]);
return 0;
}
#include
const int maxn = 1010, mod = 1000000007;
int f[maxn][maxn], N;
int main() {
scanf("%d", &N);
f[0][0] = 1;
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= i; j++) {
f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;
}
}
int ans = 0;
for (int j = 1; j <= N; j++) ans = (ans + f[N][j]) % mod;
printf("%d\n", ans);
return 0;
}
有一个易错点。因为这次是有负数的,所以要吧
f
f
f 初始化为
−
I
N
F
-INF
−INF,这个和代码设计有关。
感觉数字三角形还是从下往上走比较简单。
void solve() {
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 ans = -INF;
for (int j = 1; j <= N; j++) ans = max(ans, f[N][j]);
printf("%d\n", ans);
}
#include
using namespace std;
const int maxn = 15;
int N, f[maxn * 2][maxn][maxn], w[maxn][maxn];
int main() {
scanf("%d", &N);
int x, y, c;
while (scanf("%d%d%d", &x, &y, &c) && x) {
w[x][y] = c;
}
for (int k = 2; k <= 2 * N; k++) {
for (int i1 = 1; i1 <= N; i1++) {
for (int i2 = 1; i2 <= N; i2++) {
int j1 = k - i1, j2 = k - i2;
if (j1 < 1 || j1 > N || j2 < 1 || j2 > N) continue;
int& x = f[k][i1][i2];
int t = w[i1][j1];
if (i1 != i2) t += w[i2][j2];
x = max(x, f[k - 1][i1 - 1][i2 - 1] + t);
x = max(x, f[k - 1][i1 - 1][i2] + t);
x = max(x, f[k - 1][i1][i2 - 1] + t);
x = max(x, f[k - 1][i1][i2] + t);
}
}
}
printf("%d\n", f[2 * N][N][N]);
return 0;
}


动态规划嘛,看看需要几维才能把状态表示出来。先考虑1维。一维不够再上升。其实几维不代表循环就有几重。
朴素版:
void solve() {
for (int i = 1; i <= N; i++) {
f[i] = 1;
for (int j = 1; j <= i; j++) {
if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1);
}
}
int ans = 0;
//千万小心,答案是取max,不是f[N].
for (int i = 1; i <= N; i++) ans = max(ans, f[i]);
printf("%d\n", ans);
}
寻找公共子序列(倒着输出):
void solve() {
for (int i = 1; i <= N; i++) {
f[i] = 1, g[i] = 0;
for (int j = 1; j <= i; j++) {
if (a[j] < a[i]) {
if (f[i] < f[j] + 1) {
g[i] = j;
f[i] = f[j] + 1;
}
}
}
}
int k = 0;
for (int i = 1; i <= N; i++) {
if (f[i] > f[k]) k = i;
}
printf("%d\n", f[k]);
for (int i = 0, len = f[k]; i < len; i++) {
printf("%d ", a[k]);
k = g[k];
}
}
优化版:
void solve() {
fill(f, f + N, INF);
for (int i = 1; i <= N; i++) {
*lower_bound(f, f + N, a[i]) = a[i];
}
printf("%d\n", lower_bound(f, f + N, INF) - f);
}
for (int i = 1; i <= N; i++) {
f[i] = a[i];
for (int j = 1; j <= i; j++) {
if (a[j] < a[i]) f[i] = max(f[i], f[j] + a[i]);
}
}
int ans = 0;
for (int i = 1; i <= N; i++) ans = max(ans, f[i]);
while (scanf("%d", &a[++N]) != -1);小心!如果这样读入的话,那么 N 的值会比实际情况多1。因为最后一次循环虽然没有读入数据,但是 N 仍然增加了1.#include
#include
#include
using namespace std;
const int maxn = 1010, INF = 0x3f3f3f3f;
int a[maxn], f[maxn], N;
int main() {
int x;
while (scanf("%d", &x) != -1) {
a[N++] = x;
}
memset(f, 0x3f, sizeof f);
for (int i = 0; i < N; i++) {
*lower_bound(f, f + N, a[i]) = a[i];
}
int ans2 = lower_bound(f, f + N, INF) - f;
reverse(a, a + N);
memset(f, 0x3f, sizeof f);
for (int i = 0; i < N; i++) {
*upper_bound(f, f + N, a[i]) = a[i];
}
int ans1 = lower_bound(f, f + N, INF) - f;
printf("%d\n%d\n", ans1, ans2);
return 0;
}
int cnt = 0;
for (int i = 0; i < N; i++) {
int k = 0;
//其实,f一定是单调上升的。这个和最长上升子序列贪心解法是等价的。
while (k < cnt && f[k] < a[i]) k++;
f[k] = a[i];
if (k >= cnt) cnt++;
}
printf("%d\n", cnt);
题意:一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。
这个题很妙的地方是,巧用贪心,up数组一定是单调递减的,如果一个数字可以放进来,一定要放到尽可能考前的组里面去。
#include
#include
#include
using namespace std;
const int maxn = 55;
int a[maxn], up[maxn], down[maxn], ans, N;
void dfs(int u, int su, int sd) {
if (su + sd >= ans) return;
if (u == N) {
ans = su + sd;
return;
}
//放在上升序列里面,这个序列存的是所有上升序列的末尾的值。
int k = 0;
while (k < su && up[k] >= a[u]) k++; //注意这个up是单调下降的
int t = up[k];
up[k] = a[u];
if (k < su) dfs(u + 1, su, sd);
else dfs(u + 1, su + 1, sd);
up[k] = t;
//放在下降序列里面,这个序列存的是所有下降序列的末尾的值。
k = 0;
while (k < sd && down[k] <= a[u]) k++; //注意这个down是单调上升的。
t = down[k];
down[k] = a[u];
if (k < sd) dfs(u + 1, su, sd);
else dfs(u + 1, su, sd + 1);
down[k] = t;
}
int main() {
while (scanf("%d", &N) && N) {
//不用将up和down初始化。回溯法原本就回溯到的初始态,而且这道题有su和sd的限制,不用初始化。
ans = N;
for (int i = 0; i < N; i++) {
scanf("%d", &a[i]);
}
dfs(0, 0, 0);
printf("%d\n", ans);
}
return 0;
}
#include
#include
#include
using namespace std;
const int maxn = 55;
int a[maxn], up[maxn], down[maxn], N;
bool dfs(int u, int su, int sd, int max_depth) {
if (su + sd > max_depth) return false;
if (u == N) return true;
//放在上升序列里面
int k = 0;
while (k < su && up[k] >= a[u]) k++;
int t = up[k];
up[k] = a[u];
if (k < su && dfs(u + 1, su, sd, max_depth)) return true;
if(k >= su && dfs(u + 1, su + 1, sd, max_depth)) return true;
up[k] = t;
//放在下降序列里面
k = 0;
while (k < sd && down[k] <= a[u]) k++;
t = down[k];
down[k] = a[u];
if (k < sd && dfs(u + 1, su, sd, max_depth)) return true;
if (k >= sd && dfs(u + 1, su, sd + 1, max_depth)) return true;
down[k] = t;
return false;
}
int main() {
while (scanf("%d", &N) && N) {
for (int i = 0; i < N; i++) {
scanf("%d", &a[i]);
}
int depth = 1;
while(!dfs(0, 0, 0, depth)) depth++;
printf("%d\n", depth);
}
return 0;
}
求 A A A 和 B B B 的最长公共上升子序列, n ≤ 3000. n \le 3000. n≤3000.
设 f ( i , j ) f(i, j) f(i,j) 为 a a a 的前 i i i 项, b b b 的前 j j j 项,且以 b [ j ] b[j] b[j] 结尾的最长公共上升子序列. 难点在于当 a [ i ] = b [ j ] a[i] = b[j] a[i]=b[j] 时迅速找到所有满足 b [ k ] < b [ j ] b[k] < b[j] b[k]<b[j] 的 f(i. k) 的最大值. 但是我们注意到 a [ i ] = b [ j ] a[i] = b[j] a[i]=b[j],因此只需找到 b [ k ] < a [ i ] b[k] < a[i] b[k]<a[i] 且 f ( i , k ) f(i,k) f(i,k) 最大值即可。这个可以在每一层 i i i 固定的循环中记录下来, f ( i , j ) f(i,j) f(i,j) 可以从这个地方转移过来.
当 f ( i , j ) f(i, j) f(i,j) 不包含 a [ i ] a[i] a[i] 时, f ( i , j ) = f ( i − 1 , j ) f(i, j) = f(i-1, j) f(i,j)=f(i−1,j);当包含 a [ i ] a[i] a[i] 时, f ( i , j ) = m a x { f ( i , 1 ) , f ( i , 2 ) . . . f ( i , j − 1 ) } + 1 f(i, j) = max\{f(i, 1), f(i, 2) ... f(i,j-1)\}+1 f(i,j)=max{f(i,1),f(i,2)...f(i,j−1)}+1.
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
f[i][j] = f[i - 1][j];
if (a[i] == b[j]) {
f[i][j] = max(f[i][j], 1);
for (int k = 1; k < j; k++) {
if (b[k] < b[j]) f[i][j] = max(f[i][j], f[i][k] + 1);
}
}
}
}
#include
#include
using namespace std;
const int maxn = 3010;
int N, a[maxn], b[maxn], f[maxn][maxn];
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
for (int j = 1; j <= N; j++) scanf("%d", &b[j]);
for (int i = 1; i <= N; i++) {
//maxv 表示所有 a[i] > b[k](k从1到j-1)的f[i][j] + 1的最大值。
int maxv = 1;
for (int j = 1; j <= N; j++) {
f[i][j] = f[i - 1][j];
if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
if (b[j] < a[i]) maxv = max(maxv, f[i][j] + 1);
}
}
int ans = 0;
for (int j = 1; j <= N; j++) ans = max(ans, f[N][j]);
printf("%d\n", ans);
return 0;
}
#include
#include
using namespace std;
const int maxn = 1010;
int f[maxn][maxn], N, M;
char s1[maxn], s2[maxn];
void solve() {
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= M; j++) {
if (s1[i] == s2[j]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
printf("%d\n", f[N][M]);
}
int main() {
scanf("%d%d%s%s", &N, &M, s1 + 1, s2 + 1);
solve();
return 0;
}
题意:给定两个字符串 A A A 和 B B B,现在要将 A A A 经过若干操作变为 B B B,可进行的操作有:
现在请你求出,将 A A A 变为 B B B 至少需要进行多少次操作。
void solve() {
for (int i = 0; i <= N; i++) f[i][0] = i;
for (int j = 0; j <= M; j++) f[0][j] = j;
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= M; j++) {
//第一个是删(本来i-1和j匹配,删掉第i位使其继续匹配),第二个是增(本来是i和j-1匹配,在增加一个和第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]);
}
#include
#include
#include
using namespace std;
const int maxn = 310;
int N, M, f[maxn][maxn], h[maxn][maxn];
int dx[] = { 1, -1, 0, 0 }, dy[] = { 0, 0, 1, -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 nx = x + dx[i], ny = y + dy[i];
if (1 <= nx && nx <= N && 1 <= ny && ny <= M && h[nx][ny] < h[x][y]) {
v = max(v, dp(nx, ny) + 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;
}
OI 中所说的“自动机”一般都指“确定有限状态自动机”。
自动机是一个对信号序列进行判定的数学模型。
“信号序列”是指一连串有顺序的信号(数组从 1 1 1 到 n n n,字符串从前到后每一个字符等),“判定”是指针对某一个命题给出或真或假的回答。自动机只是一个 数学模型,而 不是算法,也 不是数据结构。实现同一个自动机的方法有很多种,可能会有不一样的时空复杂度。
一个 确定有限状态自动机(DFA) 由以下五部分构成:
DFA 的作用就是识别字符串,一个自动机 A A A,若它能识别(接受)字符串 S S S,那么 A ( S ) = T r u e A(S)=\mathrm{True} A(S)=True,否则 A ( S ) = F a l s e A(S)=\mathrm{False} A(S)=False。
当一个 DFA 读入一个字符串时,从初始状态起按照转移函数一个一个字符地转移。如果读入完一个字符串的所有字符后位于一个接受状态,那么我们称这个 DFA 接受 这个字符串,反之我们称这个 DFA 不接受 这个字符串。
如果一个状态 v v v 没有字符 c c c 的转移,那么我们令 δ ( v , c ) = n u l l \delta(v,c)=\mathrm{null} δ(v,c)=null,而 n u l l \mathrm{null} null 只能转移到 n u l l \mathrm{null} null,且 n u l l \mathrm{null} null 不属于接受状态集合。无法转移到任何一个接受状态的状态都可以视作 n u l l \mathrm{null} null,或者说, n u l l \mathrm{null} null 代指所有无法转移到任何一个接受状态的状态。
我们扩展定义转移函数 δ \delta δ,令其第二个参数可以接收一个字符串: δ ( v , s ) = δ ( δ ( v , s [ 1 ] ) , s [ 2.. ∣ s ∣ ] ) \delta(v,s)=\delta(\delta(v,s[1]),s[2..|s|]) δ(v,s)=δ(δ(v,s[1]),s[2..∣s∣]),扩展后的转移函数就可以表示从一个状态起接收一个字符串后转移到的状态。那么, A ( s ) = [ δ ( s t a r t , s ) ∈ F ] A(s)=[\delta(start,s)\in F] A(s)=[δ(start,s)∈F]。
字典树 是大部分 OIer 接触到的第一个自动机,接受且仅接受指定的字符串集合中的元素。
转移函数就是 Trie 上的边,接受状态是将每个字符串插入到 Trie 时到达的那个状态。
KMP 算法 可以视作自动机,基于字符串 s s s 的 KMP 自动机接受且仅接受以 s s s 为后缀的字符串,其接受状态为 ∣ s ∣ |s| ∣s∣。
转移函数:
δ
(
i
,
c
)
=
{
i
+
1
s
[
i
+
1
]
=
c
0
s
[
1
]
≠
c
∧
i
=
0
δ
(
π
(
i
)
,
c
)
s
[
i
+
1
]
≠
c
∧
i
>
0
\delta(i, c)=
AC 自动机 接受且仅接受以指定的字符串集合中的某个元素为后缀的字符串。也就是 Trie + KMP。
后缀自动机 接受且仅接受指定字符串的后缀。
广义后缀自动机 接受且仅接受指定的字符串集合中的某个元素的后缀。也就是 Trie + SAM。
广义 SAM 与 SAM 的关系就是 AC 自动机与 KMP 自动机的关系。
回文自动机 比较特殊,它不能非常方便地定义为自动机。
如果需要定义的话,它接受且仅接受某个字符串的所有回文子串的 中心及右半部分。
“中心及右边部分”在奇回文串中就是字面意思,在偶回文串中定义为一个特殊字符加上右边部分。这个定义看起来很奇怪,但它能让 PAM 真正成为一个自动机,而不仅是两棵树。
序列自动机 接受且仅接受指定字符串的子序列。
#include
#include
using namespace std;
const int maxn = 100010, INF = 0x3f3f3f3f;
int f[maxn][2], w[maxn], N;
int main() {
int T;
scanf("%d", &T);
f[0][0] = 0, f[0][1] = -INF;
while (T--) {
scanf("%d", &N);
for (int i = 1; i <= N; i++) scanf("%d", &w[i]);
for (int i = 1; i <= N; i++) {
f[i][0] = max(f[i - 1][0], f[i - 1][1]);
f[i][1] = f[i - 1][0] + w[i];
}
printf("%d\n", max(f[N][0], f[N][1]));
}
return 0;
}
给定一个字符串
T
T
T,和一个整数
N
N
N
现需设计一个密码
S
S
S,
S
S
S 需满足:
S
S
S 的长度是
N
N
N,
N
≤
50
N \le 50
N≤50
S
S
S 只包含小写英文字母
S
S
S 不包含子串
T
T
T
求该密码的 方案数,答案对
1
0
9
+
7
10^9+7
109+7 取模
考虑 KMP 自动机转移函数
δ
(
i
,
c
)
=
{
i
+
1
s
[
i
+
1
]
=
c
0
s
[
1
]
≠
c
∧
i
=
0
δ
(
π
(
i
)
,
c
)
s
[
i
+
1
]
≠
c
∧
i
>
0
\delta(i, c)=
因此,对于主串的第
i
i
i 个字符,有
26
26
26 中取值
a
∼
z
a \sim z
a∼z,每一种取值(回忆 KMP 匹配过程),都可以跳到模式串的某一个位置。
因此,可以对模式串搭建一个 KMP 自动机,输入的有序信号序列就是主串,状态集合即为模式串的每一个字符,转移函数相当于主串对应的字符,起始状态是模式串第 0 0 0 个,接受态是第 0 0 0 到第 m − 1 m-1 m−1 个位置,第 m m m 个位置不是接受态,因为如果到了第 m m m 个位置意味着匹配成功.
#include
using namespace std;
const int N = 55, mod = 1e9 + 7;
int f[N][N], ne[N];
char str[N];
int main()
{
int n;
scanf("%d", &n);
scanf("%s", str + 1);
int m = strlen(str + 1);
for(int i = 2, j = 0; i <= n; i++)
{
while(j && str[i] != str[j + 1]) j = ne[j];
if(str[i] == str[j + 1]) j++;
ne[i] = j;
}
f[0][0] = 1;
for(int i = 0; i < n; i++)
{
for(int j = 0; j < m; j++)
{
for(int k = 'a'; k <= 'z'; k++)
{
int u = j;
while(u && k != str[u + 1]) u = ne[u];
if(k == str[u + 1]) u++;
if(u < m) f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod;
}
}
}
int ans = 0;
for(int i = 0; i < m; i++) ans = (ans + f[n][i]) % mod;
printf("%d\n", ans);
}
#include
using namespace std;
const int N = 1010;
int get(char c)
{
if(c == 'A') return 0;
if(c == 'G') return 1;
if(c == 'T') return 2;
return 3;
}
int tr[N][4], ne[N], dar[N], idx;
char str[N];
int n, kase, q[N];
void insert()
{
int p = 0;
for(int i = 0; str[i]; i++)
{
int k = get(str[i]);
if(!tr[p][k]) tr[p][k] = ++idx;
p = tr[p][k];
}
dar[p] = 1;
}
void build()
{
int hh = 0, tt = -1;
for(int k = 0; k < 4; k++)
{
if(tr[0][k]) q[++tt] = tr[0][k];
}
while(hh <= tt)
{
int t = q[hh++];
for(int k = 0; k < 4; k++)
{
int p = tr[t][k];
if(!p) tr[t][k] = tr[ne[t]][k];
else
{
ne[p] = tr[ne[t]][k];
q[++tt] = p;
dar[p] |= dar[ne[p]];
}
}
}
}
int f[N][N];
int main()
{
while(cin >> n, n)
{
memset(tr, 0, sizeof tr);
memset(ne, 0, sizeof ne);
memset(dar, 0, sizeof dar);
idx = 0;
for(int i = 1; i <= n; i++)
{
scanf("%s", str);
insert();
}
build();
scanf("%s", str + 1);
int m = strlen(str + 1);
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
for(int i = 0; i < m; i++)
{
for(int j = 0; j <= idx; j++)
{
for(int k = 0; k < 4; k++)
{
int t = (get(str[i + 1]) != k);
int p = tr[j][k];
if(!dar[p]) f[i + 1][p] = min(f[i + 1][p], f[i][j] + t);
}
}
}
int res = 0x3f3f3f3f;
for(int i = 0; i <= idx; i++)
{
res = min(res, f[m][i]);
}
if(res == 0x3f3f3f3f) res = -1;
printf("Case %d: %d\n", ++kase, res);
}
return 0;
}
for (int i = 0; i < maxm; i++) {
int cnt = 0;
st[i] = true;
for (int j = 0; j < maxn; j++) {
if (i >> j & 1) {
if (cnt & 1) st[i] = false;
cnt = 0;
}
else cnt++;
}
if (cnt & 1) st[i] = false;
}
const int maxn = 12;
const int maxm = 1 << 12;
bool st[maxm]; //表示一种二进制状态是否存在连续的奇数个0.
int N, M;
long long f[maxn][maxm]; //强调,f第一维存的是列,第二维存的是二进制状态。
void solve() {
memset(f, 0, sizeof f);
//这一步是预处理,看看某一个二进制数字有没有连续奇数个0,有的话st就是false,反之就是true.
for (int i = 0; i < 1 << N; i++) {
int cnt = 0;
st[i] = true;
for (int j = 0; j < N; j++) {
if (i >> j & 1) {
if (cnt & 1) st[i] = false;
cnt = 0;
}
else cnt++;
}
if (cnt & 1) st[i] = false;
}
f[0][0] = 1; //因为第一列全是0属于一种情况,上一列还没有捅过来小方格。
//注意,列的下标从0开始,第一列不需要枚举。
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];
}
}
}
//第M + 1列的全0状态就是方案数,就是左边一个小方格也不能捅过来(即第M + 1列什么也别放)
printf("%lld\n", f[M][0]);
}
#include
#include
#include
#include
using namespace std;
//maxn 开到 12 的原因是,第 0 行不用,答案应该是 f(N + 1, K, 0), 即第 N + 1 行什么也不摆。
const int maxn = 12, maxm = 1 << 10, maxk = 110;
int N, K;
vector<int> state; //储存所有合法状态。
int cnt[maxm]; //储存每一个状态的1的个数。
vector<int> head[maxm]; //每一个状态可以转移到的其他状态。
typedef long long ll;
ll f[maxn][maxk][maxm]; //状态的方案数。
//检查这个状态是不是存在连续的两个1.有两个1返回false,没有的话返回true.
bool check(int st) {
for (int i = 0; i < N; i++) {
if ((st >> i & 1) && (st >> i + 1 & 1)) return false;
}
return true;
}
//计算该状态有多少个1
int calculate(int st) {
int res = 0;
for (int i = 0; i < N; i++) res += st >> i & 1;
return res;
}
int main() {
cin >> N >> K;
//预处理所有状态。
for (int i = 0; i < 1 << N; i++) {
if (check(i)) {
state.push_back(i);
cnt[i] = calculate(i); //计算该状态有多少个1,其实就是计算该状态有多少个国王。
}
}
for (int i = 0; i < state.size(); i++) {
for (int j = 0; j < state.size(); j++) {
int a = state[i], b = state[j];
if ((a & b) == 0 && check(a | b)) {
//这份代码种的 head 索引和值均为state。
head[a].push_back(b);
}
}
}
f[0][0][0] = 1;
for (int i = 1; i <= N + 1; i++) {
for (int j = 0; j <= K; j++) {
for (int k = 0; k < state.size(); k++) {
int a = state[k];
for (auto b : head[a]) {
//b本身就是状态,不是state中的下标
int c = cnt[a];
if (j >= c) f[i][j][b] += f[i - 1][j - c][a];
}
}
}
}
//最后答案是 f(N + 1, K, 0), 即第 N + 1 行什么也不摆,这就是最后的方案总数。
cout << f[N + 1][K][0] << endl;
return 0;
}
题意:农夫约翰的土地由 M ∗ N M*N M∗N 个小方格组成,现在他要在土地里种植玉米。部分土地是不育的,无法种植。而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。现在给定土地的大小,请你求出共有多少种种植方法。
我们一行一行地摆,那么正在摆的这一行只和上一行有关系。这个和小国王那个题很相似。
f ( i , j ) f(i,j) f(i,j):摆了前 i i i 行,且第 i i i 行的状态是 j j j 的方案数。假设第 i − 1 i - 1 i−1 行的状态是 a,第 i i i 行的状态是 b b b。合法的状态是:
#include
#include
#include
#include
using namespace std;
//maxn 开到14和小国王那道题是一样的。
const int maxn = 14, maxm = 1 << 12, mod = 1e8;
int N, M;
//g 表示第 i 行的禁止摆放的状态。
int g[maxn];
vector<int> state;
vector<int> head[maxm];
int f[maxn][maxm];
// 检查一个状态有无相邻的两个1. 有的话返回false, 没有的话返回 true.
bool check(int st) {
for (int i = 0; i < M; i++) {
if ((st >> i & 1) && (st >> i + 1 & 1)) {
return false;
}
}
return true;
}
int main() {
cin >> N >> M;
for (int i = 1; i <= N; i++) {
for (int j = 0; j < M; j++) {
int t;
cin >> t;
g[i] += !t << j;
}
}
for (int i = 0; i < 1 << M; i++) {
if (check(i)) {
state.push_back(i);
}
}
//head保存的是state,不是下标。
for (int i = 0; i < state.size(); i++) {
for (int j = 0; j < state.size(); j++) {
int a = state[i], b = state[j];
if ((a & b) == 0) {
head[a].push_back(b);
}
}
}
f[0][0] = 1;
for (int i = 1; i <= N + 1; i++) {
for (int j = 0; j < state.size(); j++) {
int a = state[j];
for (int b : head[a]) {
//非法位置不能放东西.
if ((g[i] & b) == 0) f[i][b] = (f[i][b] + f[i - 1][a]) % mod;
}
}
}
cout << f[N + 1][0] << endl;
return 0;
}
const int maxn = 20, maxm = 1 << maxn;
int f[maxm][maxn], w[maxn][maxn];
void solve() {
memset(f, 0x3f, sizeof f);
f[1][0] = 0;
for (int i = 0; i < 1 << N; i++) {
for (int j = 0; j < N; j++) {
if (i >> j & 1) {
for (int k = 0; k < N; k++) {
//之前的路径不可以经过j。因为要不重不漏的经过每一个节点。
//说白了就是路径把j扣掉,因为更新的是第 k 个点,因此状态还要包含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]);
}
#include
using namespace std;
const int N = 310;
int s[N], f[N][N];
int n;
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d", &s[i]);
s[i] += s[i - 1];
}
memset(f, 0x3f, sizeof f);
for(int i = 1; i <= n; i++) f[i][i] = 0;
for(int len = 2; len <= n; len++)
{
for(int l = 1, r = l + len - 1; r <= n; l++, r++)
{
for(int k = l; k + 1 <= r; 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;
}
将 n n n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。请编写一个程序,读入堆数 n n n 及每堆的石子数,并进行如下计算:
**怎样处理环?**我们将这条链延长两倍,变成 2 × n 2\times n 2×n 堆,其中第 i i i 堆与第 n + i n+i n+i 堆相同,用动态规划求解后,取 f ( 1 , n ) , f ( 2 , n + 1 ) , . . . , f ( i , n + i − 1 ) f(1,n),f(2,n+1),...,f(i,n+i-1) f(1,n),f(2,n+1),...,f(i,n+i−1) 中的最优值,即为最后的答案。时间复杂度 O ( n 3 ) O(n^3) O(n3)。
#include
using namespace std;
const int N = 410, INF = 0x3f3f3f3f;
int f[N][N], g[N][N], s[N], w[N];
int n;
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d", &w[i]);
w[n + i] = w[i];
}
for(int i = 1; i <= 2 * n; i++) s[i] = s[i - 1] + w[i];
memset(f, 0x3f, sizeof f);
memset(g, -0x3f, sizeof g);
for(int i = 1; i <= 2 * n; i++) f[i][i] = g[i][i] = 0;
for(int len = 2; len <= n; len++)
{
for(int l = 1, r = l + len - 1; r <= 2 * n; l++, r++)
{
for(int k = l; k < r; k++)
{
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
}
}
}
int res1 = INF, res2 = -INF;
for(int l = 1, r = l + n - 1; r <= 2 * n; l++, r++)
{
res1 = min(res1, f[l][r]);
res2 = max(res2, g[l][r]);
}
printf("%d\n%d\n", res1, res2);
return 0;
}