来更新一下,既然想写一篇动态规划,那么怎么能少了01背包。
主要是我 01背包顺推逆推每次都要重新理解一次
先奉上题目:
https://www.acwing.com/problem/content/2/
我们设
f
i
,
j
f_{i,j}
fi,j为前
i
i
i个物品容量为
j
j
j能收获的最大贡献。那么
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)
fi,j=max(fi−1,j,fi,−1,j−vi+wi)
这个式子表示对于第
i
i
i 件物品,选择它或不选它能产生的最大价值。
它的状态完全由前
i
−
1
i-1
i−1 个物品的状态转移而来。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 10;
int dp[maxn][maxn],w[maxn],v[maxn];
signed main(){
int n,m;
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++){
if(j>=v[i]){
dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - v[i]] + w[i]);
}
else dp[i][j] = dp[i - 1][j];
}
}
cout<< dp[n][m] <<endl;
return 0;
}
这里的时间无法再优化,但空间复杂度还可以再优化,我们注意到,当前的状态(假设为 i i i)只与 i − 1 i-1 i−1的状态有关。那么我们另设一个数组存 i − 1 i - 1 i−1的状态,那么就可以将空间复杂度由 O ( n m ) O(nm) O(nm)降至 O ( 2 m ) O(2m) O(2m)。 f [ j ] = max ( s t a [ j ] , s t a [ j − v [ i ] ] + w [ i ] ) f[j]=\max(sta[j],sta[j-v[i]]+w[i]) f[j]=max(sta[j],sta[j−v[i]]+w[i])
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 10;
int dp[maxn], w[maxn], v[maxn], sta[maxn];
signed main() {
int n, m;
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++) {
if (j >= v[i]) {
dp[j] = max(sta[j], sta[j - v[i]] + w[i]);
} else dp[j] = sta[j];
}
for (int j = 1; j <= m; j++)
sta[j] = dp[j];
}
cout << dp[m] << endl;
return 0;
}
很多初学者不明白01背包顺着推和逆着推的到底是什么逻辑。
现在我们再来优化一下上面的代码,来看看滚动数组是怎么逆推实现的。
上面我们要用
s
t
a
sta
sta 数组来记录前一个状态,现在,来试试不用
s
t
a
sta
sta 来记录状态。
我们只用
D
P
[
V
]
DP[V]
DP[V]来更新答案。
想想为什么我们要用
s
t
a
sta
sta 来记录前一个状态,因为对于现在的
d
p
[
j
]
dp[j]
dp[j],我们是对第
i
i
i 件物品选择或不选,如果顺推那就是:
d
p
[
j
]
=
max
(
d
p
[
j
]
,
d
p
[
j
−
v
[
i
]
]
+
w
[
i
]
)
dp[j]=\max(dp[j],dp[j-v[i]]+w[i])
dp[j]=max(dp[j],dp[j−v[i]]+w[i])这个式子里,
j
j
j 之前的状态已经讨论过
i
i
i 是否选取,也就是说,很有可能我当前选了
i
i
i,但是之前的
d
p
[
j
−
v
[
i
]
]
dp[j-v[i]]
dp[j−v[i]]也选过
i
i
i,造成了重复计数。
面对这种情况,就有了我们的逆推,逆着推,后面的先考虑是否选
i
i
i ,那么前面的
d
p
[
j
]
dp[j]
dp[j] 就没有被污染了。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 10;
int dp[maxn], w[maxn], v[maxn], sta[maxn];
signed main() {
int n, m;
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 >= 1; j--) {
if (j >= v[i]) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
}
cout << dp[m] << endl;
return 0;
}
闲着无聊 写一篇二进制优化dp玩玩:
题目链接
有 N 种物品和一个容量是 V
的背包。
第 i种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。接下来有 N行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i种物品的体积、价值和数量。
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
二进制优化背包的前提是你要学会01背包和多重背包。
01背包很简单吧,第i件物品,我选或是不选,当前最多能拿到多少钱(价值)。
然后我们再进行第i件物品的选择。
对于多重背包来说 ,我门对前i种物品进行选择,对于第i种物品来说,我会尝试不选,看看现在我的钱(价值)有没有增多。
选择一个,看看现在我的钱(价值)有没有增多。
再看看选两个有没有增多,再看看选三个有没有增多,直到我的包太小装不下了,就完了。
然后我们再进行i+1种物品的选择。
有没有发现有点相似?
如果我们把多重背包拆开了,一种物品有n个,我们就拆成n个独立的物品。
卧槽,这不就是01背包吗。
然后我又开始搜寻宝藏了。
然后我发现,由于我把多重背包拆开了,有时候就会遇到,有1023个相同的物品。但我只能一次一次的进行选择,yes or no,进行1023次。
Oh my god !! 明明它们都是相同的,我就不能简化吗,为什么要这样折磨我。
我不知道从哪里得到了一个结论:
任何数都可以用多个不同的2的指数进行表示。
比如 7=1+2+4
11=8+1+2
然后我就突然得到了上帝的智慧,于是我把这1023个相同物品分成了1个,2个,4个,8个,16个,32个,64个,128个,256个,512个。总共10堆,刚好分完。哇,少了好多。
如果我现在背包可以装54个这样的物品,很显然我装满就是价值最大的时候了。
咦,这上面并没有54呢,但是我一个一个装是可以装到54的呢。
别急,
还记得我们的结论吗,54=32+16+4+2;
我选了54个物品就相当于是我选了32,16,4,2,这是可以组合的,那么对于dp来说它并不会管你如何组合,只会问你能不能组合。
我们对这一数进行dp时,其实我们已经讨论了1-1023所有的数。
哈哈哈,写的可能不是很好,但已经尽力了,这是我所能理解的二进制优化,当然还有点漏洞,比如我故意取了一个特殊值,1023,它恰好被分完了,如果是1024能? 我就不细说了,欢迎大家讨论。
后面是题解:
首先就是拆分多重背包,变成01背包,相同的物品进行二进制优化拆分,会拆成很少几个部分。
所有拆分当然是先要预处理啊:
int len=1;
for (int i = 1; i <= n; i++) {
int z = 1;
for (int j = 0; s[i] >(z<<j); s[i] -= (z<<j),j++ ) {
q[len]=(z<<j)*v[i];
p[len++]=(z<<j)*w[i];
}
q[len]=s[i]*v[i];
p[len++]=s[i]*w[i];
}
预处理是我随意写的,可能写的不是很好看。
完整代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, x, y) for(auto i=(x);i<=(y);++i)
#define dep(i, x, y) for(auto i=(x);i>=(y);--i)
#define gcd(a, b) __gcd(a,b)
const long long mod = 1e9 + 7;
const int maxn = 2e4 + 10;
int lowbit(int x) { return x & -x; }
bool ispow(int n) { return (n & (n - 1)) == 0; }//O(1) 判断是否是 2^k(2的k次方)
int read() {
int x = 0, f = 1;
char c = getchar();
while (c < '0' || c > '9') {
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
int w[maxn];
int v[maxn];
int s[maxn];
int dp[10005];
int q[maxn],p[maxn];
signed main() {
int t;
int n, c;
cin >> n >> c;
rep(i, 1, n) {
cin >> v[i] >> w[i] >> s[i];
}
int len=1;
for (int i = 1; i <= n; i++) {
int z = 1;
for (int j = 0; s[i] >(z<<j); s[i] -= (z<<j),j++ ) {
q[len]=(z<<j)*v[i];
p[len++]=(z<<j)*w[i];
}
q[len]=s[i]*v[i];
p[len++]=s[i]*w[i];
}
for (int i = 1; i < len; i++) {
for (int j = c; j >= q[i]; j--) {
dp[j]=max(dp[j],dp[j-q[i]]+p[i]);
}
}
cout << dp[c] << endl;
return 0;
}
/***********************************************************************/
换个写法,效果一样:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, x, y) for(auto i=(x);i<=(y);++i)
#define dep(i, x, y) for(auto i=(x);i>=(y);--i)
#define gcd(a, b) __gcd(a,b)
const long long mod = 1e9 + 7;
const int maxn = 1e3 + 10;
int lowbit(int x) { return x & -x; }
bool ispow(int n) { return (n & (n - 1)) == 0; }//O(1) 判断是否是 2^k(2的k次方)
int read() {
int x = 0, f = 1;
char c = getchar();
while (c < '0' || c > '9') {
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
int w[maxn];
int v[maxn];
vector<int> a[100000];
int s[maxn];
int dp[100005];
signed main() {
int t;
int n, c;
cin >> n >> c;
rep(i, 1, n) {
cin >> v[i] >> w[i] >> s[i];
}
for (int i = 1; i <= n; i++) {
int z = 1;
for (int j = 0; s[i] >= (z<<j); s[i] -=(z<<j) ,j++) {
a[i].push_back((z<<j));
}
a[i].push_back(s[i]);
}
for (int i = 1; i <= n; i++) {
int len = a[i].size();
for (int k = 0; k < len; k++) {
for (int j = c; j >= v[i]; j--)
if (j - a[i][k] * v[i] >= 0)
dp[j] = max(dp[j], dp[j - a[i][k] * v[i]] + a[i][k] * w[i]);
}
}
cout << dp[c] << endl;
return 0;
}
如果遍历顺序不同,会产生另一种dp:
一种物品有n种取法,n种取法种只能取一次,产生的最大价值。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, x, y) for(auto i=(x);i<=(y);++i)
#define dep(i, x, y) for(auto i=(x);i>=(y);--i)
#define gcd(a, b) __gcd(a,b)
const long long mod = 1e9 + 7;
const int maxn = 1e3 + 10;
int lowbit(int x) { return x & -x; }
bool ispow(int n) { return (n & (n - 1)) == 0; }//O(1) 判断是否是 2^k(2的k次方)
int read() {
int x = 0, f = 1;
char c = getchar();
while (c < '0' || c > '9') {
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
int w[maxn];
int v[maxn];
vector<int> a[100000];
int s[maxn];
int dp[100005];
signed main() {
int t;
int n, c;
cin >> n >> c;
rep(i, 1, n) {
cin >> v[i] >> w[i] >> s[i];
}
for (int i = 1; i <= n; i++) {
int z = 1;
for (int j = 0; s[i] >= (z<<j); s[i] -=(z<<j) ,j++) {
a[i].push_back((z<<j));
}
a[i].push_back(s[i]);
}
for (int i = 1; i <= n; i++) {
int len = a[i].size();
for (int j = c; j >= v[i]; j--)
for (int k = 0; k < len; k++) {
if (j - a[i][k] * v[i] >= 0)
dp[j] = max(dp[j], dp[j - a[i][k] * v[i]] + a[i][k] * w[i]);
}
}
cout << dp[c] << endl;
return 0;
}
————————我是邪恶的分界线——————————————
背包九讲详解————真耐心的大佬
题意:一个数轴有n个村庄,有m个邮局建在村庄上,问如何建可使得每个村庄到最近的邮局距离之和最小。
思路:
设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示前
i
i
i个村庄选
j
j
j个邮局的距离和最小值。(日常套路设法)
首先注意,我们选前i个村庄并不考虑后面村庄的影响。
推下状态方程,发现并不好推。
首先它肯定要从前
j
−
1
j-1
j−1个推出。
前i个村庄选j-1个邮局选法有很多,需要全部遍历。
但我们还是发现并不好推这个东西,因为在添加新邮局时需要更新距离和最小,前面已经推出的
d
p
dp
dp 状态转移到当前,值是不一样的,需要更新它。而这显然不合理的。
我们需要前面的状态转移到当前,前面的
d
p
dp
dp 不需要经过加工,可以直接用。
那么我当前新加节点时, 在从前k个村庄j-1个邮局转移而来时,dp[k][j-1]不能进行变化,那么从k+1到i,我新加一个邮局,需要在这个段取得和值最小。怎么做?
中间加。证明很简单,自己去画一下就行了。
那么状态转移方程就是
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
j
]
,
d
p
[
k
]
[
j
−
1
]
+
X
)
dp[i][j]=min(dp[i][j],dp[k][j-1]+X)
dp[i][j]=min(dp[i][j],dp[k][j−1]+X)
其中
X
X
X就很有意思了,就是之前说的,在k+1☞i这个区间加一个邮局,得到的最小值和,那么怎么加?前面说过,加中间就行。
所以我们可以预处理出来。
d
[
i
]
[
j
]
d[i][j]
d[i][j] 表示从i到j加一个邮局距离和最小值。
rep(i,1,n-1){
rep(j,i+1,n){
int z=(j+i)/2;
for(int k=i;k<=j;k++){
d[i][j]+=abs(a[k]-a[z]);
}
}
}
初始状态为无穷大,dp[0][0] 为0。
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
完整代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i, x, y) for(int i=(x);i<=(y);++i)
#define dep(i, x, y) for(int i=(x);i>=(y);--i)
#define gcd(a, b) __gcd(a,b)
const int mod = 1e9 + 7;
const int maxn = 3e2 + 10;
int lowbit(int x) { return x & -x; }
bool ispow(int n) { return (n & (n - 1)) == 0; }//O(1) 判断是否是 2^k(2的k次方)
int read() {
int x = 0, f = 1;
char c = getchar();
while (c < '0' || c > '9') {
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
int dp[maxn][maxn];
int v[maxn],w[maxn];
vector<int>p[maxn];
int d[maxn][maxn];
int n, m;
int a[maxn];
signed main() {
cin>>n>>m;
// int root=0;
// rep(i,1,n){
// int s;cin>>s;
// if(s==-1)root=i;
// else
// p[s].emplace_back(i);
// }
rep(i,1,n){
cin>>a[i];
}
rep(i,1,n-1){
rep(j,i+1,n){
int z=(j+i)/2;
for(int k=i;k<=j;k++){
d[i][j]+=abs(a[k]-a[z]);
}
}
}
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m&&j<=i;j++){
for(int k=j-1;k<i;k++){
dp[i][j]=min(dp[i][j],dp[k][j-1]+d[k+1][i]);
//cout<<dp[i][1]<<endl;
}
}
}
cout<<dp[n][m]<<endl;
return 0;
}
分组背包问题:金明的预算
https://www.luogu.com.cn/problem/P1064
思路:简单分组背包,主件所带的附件不超过3个,那么组合方案数最多7个,60个物品,最多的组合数其实只有
60
×
7
4
\dfrac{60\times7}{4}
460×7。组件和附件作为同一组,分组背包时间复杂度大约为
O
(
n
∗
m
)
O(n*m)
O(n∗m)。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int dp[100][maxn];
struct node{
int v,w;
};
vector<node>s[maxn];
signed main() {
// freopen("P1064_1.in","r",stdin);
int n, m;
cin >> n >> m;
unordered_map<int,int >mp;
int cnt = 1;
for(int i = 1;i <= m; i++){
int x,y,z ;
cin>>x>>y>>z;
if(z == 0){
mp[i] = cnt;
s[cnt++].emplace_back(node{x,y});
}
else {
s[mp[z]].emplace_back(node{x,y});
}
}
for(int i = 1;i < cnt;i++){
int len = s[i].size();
vector<node>S;
S.clear();
S.emplace_back(node{s[i][0].v,s[i][0].v*s[i][0].w});
for(int j = 1;j < len;j++){
S.emplace_back(node{s[i][j].v+s[i][0].v, s[i][j].v*s[i][j].w+s[i][0].w* s[i][0].v});
for(int k = j + 1;k <len ;k++){
S.emplace_back(node{s[i][j].v + s[i][0].v + s[i][k].v, s[i][j].v*s[i][j].w + s[i][0].v*s[i][0].w+ s[i][k].w*
s[i][k].v});
for(int l = k + 1;l <len ;l ++){
S.emplace_back(node{s[i][j].v + s[i][0].v + s[i][k].v + s[i][l].v,
s[i][j].v*s[i][j].w + s[i][0].v*s[i][0].w + s[i][k].v*s[i][k].w+ s[i][l].v*s[i][l].w});
}
}
}
s[i] = S;
}
for(int i = 1;i< cnt ;i++){
for(int j = 1;j<= n ;j++){
int len = s[i].size();
for(int k = 0;k < len;k++){
if(j >= s[i][k].v) {
dp[i][j] = max(dp[i][j], max(dp[i - 1][j], dp[i - 1][j - s[i][k].v] + s[i][k].w));
// cout << dp[i][j] << endl;
}
else dp[i][j] = max(dp[i - 1][j],dp[i][j]);
}
}
}
cout<<dp[cnt - 1][n]<<endl;
return 0;
}
上面的代码实在太冗长了,接下来就开始玩代码了,反正今天状态不佳,估计学不了什么了。
来试试重载运算符
+
+
+:
好样的,在我一番调试下,终于找到了正确的重载方式。
首先,物品价格可以直接重载,木有问题。
但是!,物品性价比如果直接相乘相加,那么两个物品相加木有问题,三个物品相加就会重复计数:
s
1
+
s
2
=
v
1
∗
w
1
+
v
2
∗
w
2
s_1+s_2=v_1*w_1+v_2*w_2
s1+s2=v1∗w1+v2∗w2
正确
s
1
+
s
2
+
s
3
=
(
v
1
∗
w
1
+
v
2
∗
w
2
)
×
(
v
1
+
v
2
)
+
v
3
×
w
3
s_1 +s_2+s_3=(v_1*w_1+v_2*w_2)\times(v_1+v_2)+v_3\times w_3
s1+s2+s3=(v1∗w1+v2∗w2)×(v1+v2)+v3×w3
那么怎么重载呢?
就本身的性价比不变,再加上新来的就行了:
s
1
+
s
2
+
s
3
=
v
1
∗
w
1
+
v
2
∗
w
2
+
v
3
×
w
3
s_1 +s_2+s_3=v_1*w_1+v_2*w_2+v_3\times w_3
s1+s2+s3=v1∗w1+v2∗w2+v3×w3
这样的话第一件物品就必须加的是性价比。
代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int dp[100][maxn];
struct node{
int v,w;
node operator +(const node &p)const{
return node{p.v + v,p.w* p.v + w };
}
};
vector<node>s[maxn];
signed main() {
int n, m;
cin >> n >> m;
unordered_map<int,int >mp;
int cnt = 1;
for(int i = 1;i <= m; i++){
int x,y,z ;
cin>>x>>y>>z;
if(z == 0){
mp[i] = cnt;
s[cnt++].emplace_back(node{x,y * x});
}
else {
s[mp[z]].emplace_back(node{x,y});
}
}
for(int i = 1;i < cnt;i++){
int len = s[i].size();
vector<node>S;
S.clear();
S.emplace_back(node{s[i][0].v,s[i][0].w});
for(int j = 1;j < len;j++){
S.emplace_back(s[i][0] + s[i][j]);
for(int k = j + 1;k <len ;k++){
S.emplace_back(s[i][0]+ s[i][j] + s[i][k]);
for(int l = k + 1;l <len ;l ++){
S.emplace_back(s[i][0] + s[i][j] + s[i][l] + s[i][k]);
}
}
}
s[i] = S;
}
for(int i = 1;i< cnt ;i++){
for(int j = 1;j<= n ;j++){
int len = s[i].size();
for(int k = 0;k < len;k++){
if(j >= s[i][k].v) {
dp[i][j] = max(dp[i][j], max(dp[i - 1][j], dp[i - 1][j - s[i][k].v] + s[i][k].w));
}
else dp[i][j] = max(dp[i - 1][j],dp[i][j]);
}
}
}
cout<<dp[cnt - 1][n]<<endl;
return 0;
}