文章目录
洛谷P2704 [NOI2001] 炮兵阵地
题目链接
因为列数仅为10,考虑使用状态压缩,用一个数字
s
s
s表示某行的炮兵放置情况,当
s
s
s二进制的
k
k
k位为1,该行的
k
k
k列就放置了炮兵。
预处理: 因为要计算炮兵的数量,先计算每个状态
s
s
s的炮兵个数(即二进制中
1
1
1的数量)
s
u
m
[
s
]
sum[s]
sum[s]。
状态设计:
d
p
[
i
]
[
s
1
]
[
s
2
]
dp[i][s1][s2]
dp[i][s1][s2]表示前
i
i
i行已经安排好,第
i
−
1
i-1
i−1行的状态为
s
1
s1
s1,第
i
i
i行的状态为
s
1
s1
s1,可以放置的最多炮兵数量。起始状态是第一行所有合法状态。
状态转移: 其实状态转移比较简单,比较冗杂的是状态转移的判断。分三种情况:避免当前行的炮兵放置在山地上;避免当前行的炮兵相互攻击;如果有上一行(上上行),需要避免当前行和上一行(上上行)的炮兵相互攻击。如果都合法就增加当前行安排的状态的炮兵数量。
空间优化: 需要安排好前 i − 1 i-1 i−1行才能开始安排第 i i i行,也就是说第 i i i行的状态只和前 i − 1 i-1 i−1行有关,因此保存第前一行和当前行的状态就够了,第一维只需要2的长度保存当前行和前一行的状态。
#include<bits/stdc++.h>
using namespace std;
int dp[2][1<<10][1<<10],a[110],sum[1<<10],n,m;
inline int get_sum(int x){
int ans=0;
while(x){
ans+=(x&1);
x>>=1;
}return ans;
}
inline bool check(int x,int i){
if((x&i)||(x&(x<<1))||(x&(x<<2)))return false;
return true;
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
char ch;cin>>ch;
a[i]<<=1;
a[i]+=(ch=='H');
}
}
for(int i=0;i< 1<<m;i++)
sum[i]=get_sum(i);
//初始化第一行
for(int i=0;i< 1<<m;i++){
if(check(i,a[0]))
dp[0][0][i]=sum[i];
}
//初始化第二行
for(int s1=0;s1< 1<<m;s1++){
if(!check(s1,a[0]))continue;
for(int s2=0;s2< 1<<m;s2++){
if(check(s2,a[1])&&!(s1&s2))
dp[1][s1][s2]=dp[0][0][s1]+sum[s2];
}
}
//更新后n-2行
for(int i=2;i<n;i++){
for(int s1=0;s1< 1<<m;s1++){
if(!check(s1,a[i-1]))continue;
for(int s2=0;s2< 1<<m;s2++){
if(!check(s2,a[i])||(s1&s2))continue;
for(int s0=0;s0< 1<<m;s0++){
if(!check(s0,a[i-2])||(s1&s0)||(s2&s0))continue;
dp[i%2][s1][s2]=max(dp[i%2][s1][s2],dp[(i-1)%2][s0][s1]+sum[s2]);
}
}
}
}
int ans=0;
for(int i=0;i< 1<<m;i++)
for(int j=0;j< 1<<m;j++)
ans=max(ans,dp[(n-1)%2][i][j]);
cout<<ans<<endl;
return 0;
}
洛谷 P1879 [USACO06NOV]Corn Fields G
题目链接
状态设计:
d
p
[
i
]
[
s
]
dp[i][s]
dp[i][s]表示前
i
i
i行已经安排好,第
i
i
i行的状态为
s
s
s的方案数。
状态转移: 需要进行三种判断:避免当前状态本身冲突,因为需要养一只🐂空一只🐂,因此
(
s
&
(
s
<
<
1
)
)
(s\&(s<<1))
(s&(s<<1)) 为0;避免把牛养在贫瘠的土地上,输入时将0和1颠倒,
a
[
i
]
a[i]
a[i]二进制的
j
j
j位为1,表示位置
(
i
,
j
)
(i,j)
(i,j)为贫瘠土地不适合养牛,因此
(
s
&
a
[
i
]
)
(s\&a[i])
(s&a[i])为0;最后还要避免前一行和当前行同列都养牛。
#include<bits/stdc++.h>
using namespace std;
const int N=12,mod=1e9;
int dp[N][1<<N],a[N];
int n,m,cnt;
inline bool check(int x,int i){
if((x&(x<<1))||(x&i))return false;
return true;
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
int x;cin>>x;
a[i]<<=1;
a[i]+=(x==0);//a[i]二进制位为1表示不能种艹
}
}
//初始化第一行
for(int i=0;i< 1<<m;i++){
if(check(i,a[0]))dp[0][i]=1;
}
//更新后n-1行
for(int i=1;i<n;i++){
for(int s1=0;s1< 1<<m;s1++){
if(!check(s1,a[i-1]))continue;
for(int s2=0;s2< 1<<m;s2++){
if((s1&s2)||!check(s2,a[i]))continue;
dp[i][s2]=(dp[i-1][s1]+dp[i][s2])%mod;
}
}
}
int ans=0;
for(int i=0;i< 1<<m;i++)
ans=(ans+dp[n-1][i])%mod;
cout<<ans<<endl;
return 0;
}
洛谷 P1896 [SCOI2005]互不侵犯
题目链接
预处理: 因为要枚举的内容很多,我们先预处理出合法的状态数
s
s
s,因为此题涉及到国王个数,顺便预处理每个状态包含国王数量
s
u
m
sum
sum。
状态设计和转移:
d
p
[
i
]
[
j
]
[
m
]
dp[i][j][m]
dp[i][j][m]表示安排完前
i
−
1
i-1
i−1行,第
i
i
i行的状态为
s
[
j
]
s[j]
s[j],且当前安排完的国王数量已经达到
m
m
m的方案数。然后判断会不会和前一行的枚举状态
s
[
l
]
s[l]
s[l]冲突,若不冲突直接加上
d
p
[
i
−
1
]
[
l
]
[
m
−
s
u
m
[
j
]
dp[i-1][l][m-sum[j]
dp[i−1][l][m−sum[j]。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=10;
LL dp[N][1<<N][N*N];
int sum[1<<N],s[1<<N];
int cnt,n,k;
//状态设计: dp[i][s][m]表示安排完前i行,第i行的状态为s,且当前安排完的国王数量已经达到m的方案数。
int main(){
cin>>n>>k;
for(int i=0;i< 1<<n;i++){
if(i&(i<<1))continue;
s[++cnt]=i;
int cur=i;
while(cur){
if(cur&1)sum[cnt]++;
cur>>=1;
}
}
for(int i=1;i<=cnt;i++)dp[1][i][sum[i]]=1;
for(int i=2;i<=n;i++){//枚举当前行数
for(int j=1;j<=cnt;j++){//枚举当前一行的状态
for(int m=0;m<=k;m++){//枚举到达当前行总共有多少个国王
if(m<sum[j])continue;
for(int l=1;l<=cnt;l++){//枚举上一行的状态
if((s[l]&s[j])||(s[l]&(s[j]<<1))||(s[l]&(s[j]>>1)))continue;
dp[i][j][m]+=dp[i-1][l][m-sum[j]];
}
}
}
}
LL ans=0;
for(int i=1;i<=cnt;i++)
ans+=dp[n][i][k];
cout<<ans<<endl;
return 0;
}
洛谷 P3092 [USACO13NOV]No Change G
题目链接
参考题解
状态设计: 因为硬币只有16个,因此可以用
s
t
st
st表示使用硬币的情况,数字
s
t
st
st的二进制
j
j
j位置为0表示第
j
j
j个硬币不使用,若其为1表示当前状态使用第
j
j
j个硬币。用
d
p
[
s
t
]
dp[st]
dp[st]表示当前硬币使用情况为
s
t
st
st时,从左到右购买物品最多可以购买到哪一个物品。仍然使用刷表法,即从当前状态可以向哪些状态转移。假设当前状态为
s
t
0
st0
st0,可以转移到
s
t
st
st,那应该满足的条件就是
s
t
0
&
(
2
j
)
=
0
st0 \& (2^j)=0
st0&(2j)=0,同时
s
t
=
s
t
0
+
(
2
j
)
st=st0+(2^j)
st=st0+(2j)。表示
s
t
0
st0
st0这个取硬币的状态再加上第
j
j
j个硬币就变成
s
t
st
st。
状态转移: 再考虑
d
p
[
s
t
0
]
dp[st0]
dp[st0]如何转移到
d
p
[
s
t
]
dp[st]
dp[st]。因为多加了第
j
j
j个硬币,那就用第
j
j
j个硬币去购买从
d
p
[
s
t
0
]
+
1
dp[st0]+1
dp[st0]+1个物品开始以右的最多物品,因为从第一个物品到第
d
p
[
s
t
0
]
dp[st0]
dp[st0]个都已经被购买了。朴素做法是从第
d
p
[
s
t
0
]
+
1
dp[st0]+1
dp[st0]+1开始向右边枚举求和达到第
j
j
j个硬币的价值。此处可以用前缀和与二分作为优化。二分可以达到的最右边的物品
R
R
R,使得
∑
i
=
d
p
[
s
t
0
]
+
1
i
=
R
v
a
l
i
\sum_{i=dp[st0]+1}^{i=R}{val_i}
∑i=dp[st0]+1i=Rvali不超过第
j
j
j个硬币的价值。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=1e5+7,K=1<<16;
int dp[K],coin[16],n,k;
LL sum[N];
inline LL cal(int s){//还没有使用的硬币就是剩下的钱
LL ans=0;
for(int i=0;i<k;i++){
if(s&(1<<i))continue;
ans+=coin[i];
}return ans;
}
int main(){
cin>>k>>n;
for(int i=0;i<k;i++)cin>>coin[i];
for(int i=1;i<=n;i++){
int x;cin>>x;
sum[i]=sum[i-1]+x;
}
LL ans=-1;
for(int s0=0;s0< 1<<k;s0++){
for(int j=0;j<k;j++){
if(s0&(1<<j))continue;
int s=s0|(1<<j);
//二分查找j硬币可以购买到最右边的货物
int l=dp[s0]+1,r=n,mid,res;
while(l<=r){
mid=(l+r)>>1;
if(sum[mid]-sum[dp[s0]]>coin[j]){
r=mid-1;
}
else {
l=mid+1;
res=mid;
}
}
//因为当前的s可能从别的s0转移过来,如果当前s0转移过来达到的最右端反而更小,就应该保存之前的结果
if(dp[s]>res)continue;
dp[s]=res;
//如果可以买完全部货物就更新答案
if(dp[s]==n)ans=max(cal(s),ans);
}
}
cout<<ans<<endl;
return 0;
}
洛谷P3694 邦邦的大合唱站队
题目链接
状态设计: 团队数量很少仅为20,考虑使用状态压缩,设
d
p
[
s
]
dp[s]
dp[s]为状态为
s
s
s的队形需要出队的最少人数。状态
s
s
s的含义是:若
s
s
s第
j
j
j个位置为1,表示第
j
j
j个团队的人都已经靠在一起。
预处理状态:
s
u
m
[
i
]
[
j
]
sum[i][j]
sum[i][j]:前
i
i
i个人中属于团队
j
j
j的人数;
l
e
n
[
s
]
len[s]
len[s]状态
s
s
s中已经排列好的人数;
n
u
m
[
j
]
num[j]
num[j]:团队
j
j
j的总人数。
状态转移:
d
p
[
s
]
dp[s]
dp[s]转移向
d
p
[
s
1
]
dp[s1]
dp[s1]:排完
s
s
s中的团队(状态s不包含第
j
j
j个队伍),
s
1
=
s
^
(
1
<
<
j
)
s1=s\hat{\space\space}(1<<j)
s1=s ^(1<<j),表示接下来将第
j
j
j个团队,排列在
s
s
s状态中已经排列好的团队之后。具体过程如下图:
其实最朴素的想法就是对团队编号进行全排列,然后计算不同排列需要出队的人数取最小值;但是全排列肯定TLE,我们把排列拆分为一层层看,第1层确定第1个位置分配给哪个团队,第2层是在确定了第1个位置分配给哪个团队后再枚举第2个位置分配给的团队…给排列那棵树多了很多剪枝,假设第
1
,
3
,
4
1,3,4
1,3,4个团队排在前面,已经知道排列
143
143
143可以使出队人数最少,那我们每次用它们继续扩展下一层时,就少了
134
,
314
,
341
,
413
,
431
134,314,341,413,431
134,314,341,413,431这几种排列的拓展。从而将原本
O
(
k
!
)
O(k!)
O(k!)的复杂度降低为
O
(
2
k
)
O(2^k)
O(2k)。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=1e5+7,M=1<<20;
int dp[M],n,m,sum[N][20],num[20],len[M];
int main(){
cin>>n>>m;
memset(dp,0x3f,sizeof dp);
for(int i=1;i<=n;i++){
int x;cin>>x;
x--;num[x]++;
for(int j=0;j<m;j++){
sum[i][j]=sum[i-1][j];
}sum[i][x]++;
}
//预处理每个状态i已经排好的总人数
for(int i=0;i< 1<<m;i++){
for(int j=0;j<m;j++){
if(i&(1<<j))len[i]+=num[j];
}
}
//起始状态是一个团队都没有排列
dp[0]=0;
for(int s=0;s< 1<<m;s++){
for(int j=0;j<m;j++){
if(s&(1<<j))continue;
int s1=s^(1<<j);
dp[s1]=min(dp[s1],dp[s]+num[j]-(sum[len[s1]][j]-sum[len[s]][j]));
}
}
cout<<dp[(1<<m)-1]<<endl;
return 0;
}