题面
题目链接:https://codeforces.ml/gym/101002
题意
- 有n种信,每种的长宽个数分别是 x i , y i , n u m i x_i,y_i,num_i xi,yi,numi。至多买k种信封,找到一种信封购买方案,使得所有信都能装入,使得总浪费最小。
- n,k都<=15,长宽个数都是1e4.。
题解
从nk的小范围就很明显看出是装压DP。所以设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示用i个信封装集合j的信的最小浪费。转移方程:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
−
1
]
[
k
]
+
p
r
e
[
j
⊕
k
]
)
,
k
⊆
j
dp[i][j]=min(dp[i-1][k]+pre[j\oplus k]),k\subseteq j
dp[i][j]=min(dp[i−1][k]+pre[j⊕k]),k⊆j
就是现在用第i中信封装k,剩下的让i-1个封装。
复杂度
复杂度自然是子集的子集的个数。大小为i的子集的子集个数是
2
i
2^i
2i,
而大小为i的子集的个数是
C
n
i
C_n^i
Cni。所以总复杂度是:
T
=
∑
i
=
0
n
C
n
i
∗
2
i
T=\sum_{i=0}^{n}C_n^i*2^i
T=i=0∑nCni∗2i明眼人一眼就能发现这是二项式展开式。所以:
T
=
∑
i
=
0
n
C
n
i
∗
2
i
=
(
2
+
1
)
n
=
3
n
T=\sum_{i=0}^{n}C_n^i*2^i=(2+1)^n=3^n
T=i=0∑nCni∗2i=(2+1)n=3n
而
3
1
5
3^15
315大概是1.5e7的数量级,所以常数写小点可以卡过。
代码
我开始不知道快速枚举子集的子集的方法,就按照意思强行模拟,复杂度不变,常数有点大,代码还不好写。丑版代码:
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int NN=16;
const long long oo=1000000000000000000ll;
long long dp[NN][(1<<16)+10];
int rec[20];
struct ppp{
int x,y,num;
}lett[NN];
long long pre[(1<<16)+10];
int n,k;
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++){
scanf("%d%d%d",&lett[i].x,&lett[i].y,&lett[i].num);
}
pre[0]=0;
for(int i=1;i<=((1<<n)-1);i++){
int temp=i;int maxx=0,maxy=0;
int ste=0;
while(temp){
if(temp%2==1){
maxx=max(lett[ste+1].x,maxx);
maxy=max(lett[ste+1].y,maxy);
}
temp/=2;ste++;
}
temp=i;ste=1;
while(temp){
if(temp%2==1){
pre[i]+=1ll*(maxx*maxy-lett[ste].x*lett[ste].y)*lett[ste].num;
}
temp/=2;ste++;
}
}
for(int i=0;i<=k;i++)for(int j=0;j<(1<<n);j++)dp[i][j]=oo;
dp[0][0]=0;
for(int i=1;i<=k;i++){
for(int j=0;j<(1<<n);j++){
int temp=j,ste=0;
int nn=0;
while(temp){
if(temp%2==1){
rec[nn]=ste;
nn++;
}
temp/=2;ste++;
}
for(int kk=0;kk<(1<<nn);kk++){
int resk=0;
int tempk=kk,stek=0;
while(tempk){
if(tempk%2==1){
resk+=(1<<rec[stek]);
}
tempk/=2;stek++;
}
dp[i][j]=min(dp[i][j],dp[i-1][resk]+pre[j^resk]);
}
}
}
printf("%lld\n",dp[k][(1<<n)-1]);
return 0;
}
然后常数太大T了
我学了位运算快速枚举的技巧,就过了,AC代码:
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int NN=16;
const long long oo=1000000000000000000ll;
long long dp[NN][(1<<16)+10];
int rec[20];
struct ppp{
int x,y,num;
}lett[NN];
long long pre[(1<<16)+10];
int n,k;
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++){
scanf("%d%d%d",&lett[i].x,&lett[i].y,&lett[i].num);
}
pre[0]=0;
for(int i=1;i<=((1<<n)-1);i++){
int temp=i;int maxx=0,maxy=0;
int ste=0;
while(temp){
if(temp%2==1){
maxx=max(lett[ste+1].x,maxx);
maxy=max(lett[ste+1].y,maxy);
}
temp/=2;ste++;
}
temp=i;ste=1;
while(temp){
if(temp%2==1){
pre[i]+=1ll*(maxx*maxy-lett[ste].x*lett[ste].y)*lett[ste].num;
}
temp/=2;ste++;
}
}
for(int i=0;i<=k;i++)for(int j=0;j<(1<<n);j++)dp[i][j]=oo;
dp[0][0]=0;
for(int i=1;i<=k;i++){
for(int j=0;j<(1<<n);j++){
int ss=j;
while(1){
dp[i][j]=min(dp[i-1][ss]+pre[j^ss],dp[i][j]);
if(ss==0)break;
ss=(ss-1)&j;
}
}
}
printf("%lld\n",dp[k][(1<<n)-1]);
return 0;
}
其中用来枚举子集的子集的板子:
while(1){
dp[i][j]=min(dp[i-1][ss]+pre[j^ss],dp[i][j]);
if(ss==0)break;
ss=(ss-1)&j;
}
ss为j的子集。