线性DP
通天之分组背包
题目背景
直达通天路·小 A 历险记第二篇
题目描述
自 01 01 01 背包问世之后,小 A 对此深感兴趣。一天,小 A 去远游,却发现他的背包不同于 01 01 01 背包,他的物品大致可分为 k k k 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。
输入格式
两个数 m , n m,n m,n,表示一共有 n n n 件物品,总重量为 m m m。
接下来 n n n 行,每行 3 3 3 个数 a i , b i , c i a_i,b_i,c_i ai,bi,ci,表示物品的重量,利用价值,所属组数。
输出格式
一个数,最大的利用价值。
样例 #1
样例输入 #1
45 3
10 10 1
10 5 1
50 400 2
样例输出 #1
10
提示
1 ≤ m , n ≤ 1000 1 \leq m, n \leq 1000 1≤m,n≤1000。
思路
其实就是01背包的增强版
做法是先枚举每个组和容量,然后在从一个组里选出一个物品来进行动态规划;
code
#include<bits/stdc++.h>
using namespace std;
int v,n,t;
int x, g[205][205], i,j,k, w[10001],z[10001], b[10001], dp[10001];
int main(){
cin>>v>>n;
for(i=1;i<=n;i++){
cin>>w[i]>>z[i]>>x;//物品重量,物品价值
t=max(t,x);//看看一共有多少组
b[x]++;//该组的物品数量+1
g[x][b[x]]=i;//该组的这个物品是第i个物品
}
for(i=1;i<=t;i++){
//挨组枚举,
for(j=v;j>=0;j--){
//背包体积,倒着算。
for(k=1;k<=b[i];k++){
//枚举该组里的物品
if(j>=w[g[i][k]]){
//还有位置就装
dp[j]=max(dp[j],dp[j-w[g[i][k]]]+z[g[i][k]]);//占一部分体积得到相应的价值
}
}
}
}
cout<<dp[v];//体积v的最大价值
return 0;
}
[HEOI2013]Eden 的新背包问题
题目背景
“ 寄 没 有 地 址 的 信 ,这 样 的 情 绪 有 种 距 离 ,你 放 着 谁 的 歌 曲 ,是 怎 样 的 心 情 。 能 不 能 说 给 我 听 。”
题目描述
失忆的 Eden 总想努力地回忆起过去,然而总是只能清晰地记得那种思念的感觉,却不能回忆起她的音容笑貌。
记忆中,她总是喜欢给 Eden 出谜题:在 valentine’s day 的夜晚,两人在闹市中闲逛时,望着礼品店里精巧玲珑的各式玩偶,她突发奇想,问了 Eden 这样的一个问题:有 n n n 个玩偶,每个玩偶有对应的价值、价钱,每个玩偶都可以被买有限次,在携带的价钱 m m m 固定的情况下,如何选择买哪些玩偶以及每个玩偶买多少个,才能使得选择的玩偶总价钱不超过 m m m,且价值和最大。
众所周知的,这是一个很经典的多重背包问题,Eden 很快解决了,不过她似乎因为自己的问题被飞快解决感到了一丝不高兴,于是她希望把问题加难:多次询问,每次询问都将给出新的总价钱,并且会去掉某个玩偶(即这个玩偶不能被选择),再问此时的多重背包的答案(即前一段所叙述的问题)。
这下 Eden 犯难了,不过 Eden 不希望自己被难住,你能帮帮他么?
输入格式
第一行有一个整数,代表玩偶的个数 n n n,玩偶从 0 0 0 开始编号。
第二行开始后面的 n n n 行,每行三个整数,第 ( i + 2 ) (i + 2) (i+2) 行的整数 a i , b i , c i a_i, b_i, c_i ai,bi,ci,分别表示买一个第 i i i 个玩偶需要的价钱,获得的价值以及第 i i i 个玩偶的限购次数。
接下来的一行有一个整数 q q q,表示询问次数。
接下来 q q q 行,每行两个整数 d i , e i d_i, e_i di,ei,表示每个询问去掉的是哪个玩偶(注意玩偶从 0 0 0 开始编号)以及该询问对应的新的总价钱数。(去掉操作不保留,即不同询问互相独立)。
输出格式
输出 q q q 行,第 i i i 行输出对于第 i i i 个询问的答案。
样例 #1
样例输入 #1
5
2 3 4
1 2 1
4 1 2
2 1 1
3 2 3
5
1 10
2 7
3 4
4 8
0 5
样例输出 #1
13
11
6
12
4
提示
样例解释
一共五种玩偶,分别的价钱价值和限购次数为 ( 2 , 3 , 4 ) (2,3,4) (2,3,4), ( 1 , 2 , 1 ) (1,2,1) (1,2,1), ( 4 , 1 , 2 ) (4,1,2) (4,1,2), ( 2 , 1 , 1 ) (2,1,1) (2,1,1), ( 3 , 2 , 3 ) (3,2,3) (3,2,3)。
五个询问,以第一个询问为例。
第一个询问表示的是去掉编号为 1 1 1 的玩偶, 且拥有的钱数为 10 10 10 时可以获得的最大价值,则此时剩余玩偶为 ( 2 , 3 , 4 (2,3,4 (2,3,4), ( 4 , 1 , 2 ) (4,1,2) (4,1,2), ( 2 , 1 , 1 ) (2,1,1) (2,1,1), ( 3 , 2 , 3 ) (3,2,3) (3,2,3),若把编号为 0 0 0 的玩偶买 4 4 4 个(即全买了),然后编号为 3 3 3 的玩偶 买一个,则刚好把 10 10 10 元全部花完,且总价值为 13 13 13。可以证明没有更优的方案了。
注意买某种玩偶不一定要买光。
数据规模与约定
- 对于 10 % 10\% 10% 的数据,保证 n ≤ 10 n \leq 10 n≤10。
- 另外存在 20 % 20\% 20% 的数据,保证 n ≤ 100 n \leq 100 n≤100, c i = 1 c_i = 1 ci=1, q ≤ 100 q \leq 100 q≤100。
- 另外存在 20 % 20\% 20% 的数据,保证 n ≤ 100 n \leq 100 n≤100, q ≤ 100 q \leq 100 q≤100。
- 另外存在 30 % 30\% 30% 的数据,保证 c i = 1 c_i = 1 ci=1。
- 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 1000 1 \leq n \leq 1000 1≤n≤1000, 1 ≤ q ≤ 3 × 1 0 5 1 \leq q \leq 3\times 10^5 1≤q≤3×105, 1 ≤ a i , b i , c i ≤ 100 1 \leq a_i,b_i,c_i \leq 100 1≤ai,bi,ci≤100, 0 ≤ d i < n 0 \leq d_i < n 0≤di<n, 0 ≤ e i ≤ 1000 0 \leq e_i \leq 1000 0≤ei≤1000。
思路
多重背包,从前dp一边,从后dp一边,然后得到两套结果,隔一个相加起来就是答案。
code
#include<bits/stdc++.h>
#define B 1010
using namespace std;
typedef long long ll;
int n, v[B], w[B], p[B], f[B][B], g[B][B], a, b, q;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> w[i] >> v[i] >> p[i];//输入花费,价值,数量
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= 1000; j++) f[i][j] = f[i - 1][j];//往后挪一个
int num = p[i];//数量
for (int k = 1; num; k <<= 1) {
if (k > num) k = num;
num -= k;
for (int j = 1000; j >= w[i] * k; j--)
f[i][j] = max(f[i][j], f[i][j - w[i] * k] + v[i] * k);//模板
}
}//从前往后dp
for (int i = n; i >= 1; i--) {
for (int j = 1; j <= 1000; j++) g[i][j] = g[i + 1][j];
int num = p[i];
for (int k = 1; num; k <<= 1) {
if (k > num) k = num;
num -= k;
for (int j = 1000; j >= w[i] * k; j--)
g[i][j] = max(g[i][j], g[i][j - w[i] * k] + v[i] * k);
}
}//从后往前dp,这两隔一个加一起就是去掉某一个后的最大价值
cin >> q;
while (q--) {
cin >> a >> b; int ans = 0;
for (int i = 0; i <= b; i++)//注意从0开始
ans = max(ans, f[a][i] + g[a + 2][b - i]);
cout << ans << endl;
}
}
[AHOI2009] 中国象棋
题目描述
这次小可可想解决的难题和中国象棋有关,在一个 n n n 行 m m m 列的棋盘上,让你放若干个炮(可以是 0 0 0 个),使得没有一个炮可以攻击到另一个炮,请问有多少种放置方法。大家肯定很清楚,在中国象棋中炮的行走方式是:一个炮攻击到另一个炮,当且仅当它们在同一行或同一列中,且它们之间恰好 有一个棋子。你也来和小可可一起锻炼一下思维吧!
输入格式
一行包含两个整数 n , m n,m n,m,之间由一个空格隔开。
输出格式
总共的方案数,由于该值可能很大,只需给出方案数模 9999973 9999973 9999973 的结果。
样例 #1
样例输入 #1
1 3
样例输出 #1
7
提示
样例说明
除了 3 3 3 个格子里都塞满了炮以外,其它方案都是可行的,所以一共有 2 × 2 × 2 − 1 = 7 2 \times 2 \times 2-1=7 2×2×2−1=7 种方案。
数据规模与约定
- 对于 30 % 30\% 30% 的数据, n n n 和 m m m 均不超过 6 6 6。
- 对于 50 % 50\% 50% 的数据, n n n 和 m m m 至少有一个数不超过 8 8 8。
- 对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 100 1 \leq n,m \leq 100 1≤n,m≤100。
思路
1. 第 i 行 什 么 都 不 放 。 太 愚 蠢 了 。 1.第i行什么都不放。太愚蠢了。 1.第i行什么都不放。太愚蠢了。
f [ i ] [ j ] [ k ] = f [ i ] [ j ] [ k ] + f [ i − 1 ] [ j ] [ k ] f[i][j][k]=f[i][j][k]+f[i-1][j][k] f[i][j][k]=f[i][j][k]+f[i−1][j][k]
2. 放 1 个 在 空 的 列 。 根 据 状 态 很 容 易 发 现 空 的 列 是 m − j − k 。 那 么 方 案 显 然 了 : 2.放1个在空的列。根据状态很容易发现空的列是m-j-k。那么方案显然了: 2.放1个在空的列。根据状态很容易发现空的列是m−j−k。那么方案显然了:
f [ i ] [ j ] [ k ] = f [ i ] [ j ] [ k ] + f [ i − 1 ] [ j − 1 ] [ k ] × ( m − ( j − 1 ) − k ) f[i][j][k]=f[i][j][k]+f[i-1][j-1][k]\times(m-(j-1)-k) f[i][j][k]=f[i][j][k]+f[i−1][j−1][k]×(m−(j−1)−k)
3. 放 1 个 在 有 1 个 的 列 。 3.放1个在有1个的列。 3.放1个在有1个的列。
f [ i ] [ j ] [ k ] = f [ i ] [ j ] [ k ] + f [ i − 1 ] [ j + 1 ] [ k − 1 ] × ( j + 1 ) f[i][j][k]=f[i][j][k]+f[i-1][j+1][k-1]\times(j+1) f[i][j][k]=f[i][j][k]+f[i−1][j+1][k−1]×(j+1)
4. 放 2 个 都 在 空 的 列 。 这 里 就 用 到 排 列 了 , 是 之 前 状 态 乘 以 C ( 之 前 空 的 列 , 2 ) 。 4.放2个都在空的列。这里就用到排列了,是之前状态乘以C(之前空的列,2)。 4.放2个都在空的列。这里就用到排列了,是之前状态乘以C(之前空的列,2)。
f [ i ] [ j ] [ k ] = f [ i ] [ j ] [ k ] + f [ i − 1 ] [ j − 2 ] [ k ] × C ( m − ( j − 2 ) − k ) f[i][j][k]=f[i][j][k]+f[i-1][j-2][k]\times C(m-(j-2)-k) f[i][j][k]=f[i][j][k]+f[i−1][j−2][k]×C(m−(j−2)−k)
5. 放 1 个 在 空 的 列 , 1 个 在 有 1 个 的 列 。 同 样 乘 法 原 理 : 5.放1个在空的列,1个在有1个的列。同样乘法原理: 5.放1个在空的列,1个在有1个的列。同样乘法原理:
f [ i ] [ j ] [ k ] = f [ i ] [ j ] [ k ] + f [ i − 1 ] [ j ] [ k − 1 ] × j × ( m − j − ( k − 1 ) ) f[i][j][k]=f[i][j][k]+f[i-1][j][k-1]\times j\times (m-j-(k-1)) f[i][j][k]=f[i][j][k]+f[i−1][j][k−1]×j×(m−j−(k−1))
6. 放 2 个 在 有 1 个 的 列 。 我 一 开 始 愚 蠢 到 忘 记 考 虑 这 种 了 。 6.放2个在有1个的列。我一开始愚蠢到忘记考虑这种了。 6.放2个在有1个的列。我一开始愚蠢到忘记考虑这种了。
f [ i ] [ j ] [ k ] = f [ i ] [ j ] [ k ] + f [ i − 1 ] [ j + 2 ] [ k − 2 ] × C ( j + 2 ) f[i][j][k]=f[i][j][k]+f[i-1][j+2][k-2]\times C(j+2) f[i][j][k]=f[i][j][k]+f[i−1][j+2][k−2]×C(j+2)
初 始 化 很 简 单 : f [ 0 ] [ 0 ] [ 0 ] = 1 初始化很简单:f[0][0][0]=1 初始化很简单:f[0][0][0]=1
code
#include<iostream>
using namespace std;
long long n,m;
long long dp[101][101][101];
long long C(long long x){
return (x*(x-1)/2)%9999973;
}
int main(){
cin>>n>>m;
dp[0][0][0]=1;
for(long long i=1;i<=n;i++){
for(long long j=0;j<=m;j++){
for(long long k=0;k<=m-j;k++){
dp[i][j][k]=dp[i-1][j][k];
if(k>=1)dp[i][j][k]+=dp[i-1][j+1][k-1]*(j+1);
if(j>=1)dp[i][j][k]+=dp[i-1][j-1][k]*(m-j-k+1);
if(k>=2)dp[i][j][k]+=dp[i-1][j+2][k-2]*(j+2)*(j+1)/2;
if(k>=1)dp[i][j][k]+=dp[i-1][j][k-1]*j*(m-j-k+1);
if(j>=2)dp[i][j][k]+=dp[i-1][j-2][k]*C(m-j-k+2);
dp[i][j][k]%=9999973;//及时取模!
}
}
}
long long ans=0;
for(long long i=0;i<=m;i++){
for(long long j=0;j<=m;j++){
ans=(ans+dp[n][i][j])%9999973;
}
}
cout<<ans%9999973;
}
棋盘分割
题目背景
无
题目描述
将一个8*8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的两部分中的任意一块继续如此分割,这样割了(n-1)次后,连同最后剩下的矩形棋盘共有n块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)
原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割成n块矩形棋盘,并使各矩形棋盘总分的平方和最小。
请编程对给出的棋盘及n,求出平方和的最小值。
输入格式
第1行为一个整数n(1 < n <= 15)。
第2行至第9行每行为8个小于100的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。
输出格式
仅一个数,为平方和。
样例 #1
样例输入 #1
3
1 1 1 1 1 1 1 3
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 3
样例输出 #1
1460
思路
用二维前缀和求出任意矩阵的和;
设置dp[a,b,c,d,e]其中a,b,c,d表示此矩阵的左上坐标和右下坐标,e表示切的位置,统计每次横切纵切的最小值,第n-1刀即为答案。
code
#include<iostream>
#define ff(i,a,b) for(int i=a;i<=b;i++)
using namespace std;
int f[15][15][15][15][15];
int n;
int sum[15][15];
int get(int a, int b, int c, int d) {
a--, b--;
int siz = sum[c][d] + sum[a][b] - sum[a][d] - sum[c][b];
return siz * siz;
}
int main() {
cin >> n;//切成 n 块
n--;//切 n - 1 次
ff(i, 1, 8)
ff(j, 1, 8){
cin >> sum[i][j];
sum[i][j] += sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1];//二维前缀和
}
ff(a, 1, 8)
ff(b, 1, 8)
ff(c, a, 8)
ff(d, b, 8) {
f[a][b][c][d][0] = get(a, b, c, d);//获得各矩阵的平方
}
ff(t, 1, n)
ff(a, 1, 8)
ff(b, 1, 8)
ff(c, a, 8)
ff(d, b, 8) {
int q = 99999999;
ff(x, a, c - 1)//横着切
q = min(q, min(f[x + 1][b][c][d][0] + f[a][b][x][d][t - 1], f[x + 1][b][c][d][t - 1] + f[a][b][x][d][0]));
ff(y, b, d - 1)//竖着切
q = min(q, min(f[a][y + 1][c][d][0] + f[a][b][c][y][t - 1], f[a][y + 1][c][d][t - 1] + f[a][b][c][y][0]));
f[a][b][c][d][t] = q;//得到最小的结果
}
cout << f[1][1][8][8][n];
}
[SCOI2005]最大子矩阵
题目描述
这里有一个n*m的矩阵,请你选出其中k个子矩阵,使得这个k个子矩阵分值之和最大。注意:选出的k个子矩阵不能相互重叠。
输入格式
第一行为n,m,k(1≤n≤100,1≤m≤2,1≤k≤10),接下来n行描述矩阵每行中的每个元素的分值(每个元素的分值的绝对值不超过32767)。
输出格式
只有一行为k个子矩阵分值之和最大为多少。
样例 #1
样例输入 #1
3 2 2
1 -3
2 3
-2 3
样例输出 #1
9
思路
1 < = m < = 2 1<=m<=2 1<=m<=2 分类讨论吧。
若 m = 1 , d p [ i , j ] m=1 ,dp[i,j] m=1,dp[i,j] 表示前i个数中取出j个矩形的最大和;
若 m = 2 , f [ i , j , k ] m=2,f[i,j,k] m=2,f[i,j,k] 表示第一列选到第i个数,第二列选到第j个数时,总共k个子矩形的答案。
转移方程还挺好理解的,把数字带到定义里看看就懂。
code
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
const int M = 11;
int n, m, K, s1[N], s2[N], dp[N][M], f[N][N][M];
int main() {
cin >> n >> m >> K;
if (m == 1) {
for (int i = 1, x; i <= n; i++) {
cin >> x;
s1[i] = s1[i - 1] + x;
}
for (int k = 1; k <= K; k++) {
for (int i = 1; i <= n; i++) {
dp[i][k] = dp[i - 1][k];
for (int j = 0; j < i; j++) dp[i][k] = max(dp[i][k], dp[j][k - 1] + s1[i] - s1[j]);
}
}
cout << dp[n][K];
} else {
for (int i = 1, x, y; i <= n; i++) {
cin >> x >> y;
s1[i] = s1[i - 1] + x, s2[i] = s2[i - 1] + y;
}
for (int k = 1; k <= K; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
f[i][j][k] = max(f[i - 1][j][k], f[i][j - 1][k]);
for (int l = 0; l < i; l++) f[i][j][k] = max(f[i][j][k], f[l][j][k - 1] + s1[i] - s1[l]);
for (int l = 0; l < j; l++) f[i][j][k] = max(f[i][j][k], f[i][l][k - 1] + s2[j] - s2[l]);
if (i == j) for (int l = 0; l < i; l++) f[i][j][k] = max(f[i][j][k], f[l][l][k - 1] + s1[i] - s1[l] + s2[j] - s2[l]);
}
}
}
cout << f[n][n][K];
}
}