前言
复习笔记第4篇。CSP RP++。
引用部分为总结性内容。
0——P1433 吃奶酪
题目链接 luogu
题意
房间里放着 \(n\) 块奶酪,要把它们都吃掉,问至少要跑多少距离?一开始在 \((0,0)\) 点处。 \(n\leq 15\) ,保留两位小数。
思路
为啥状压第一题是绿题啊。这么水了吗,为啥我还不会(
令 \(f[i][s]\) 表示从点 \(i\) 出发,遍历集合为 \(S\) 的最小值,枚举其他点进行转移。预处理边界 \(f[i][s]=0\) (\(S\) 为除了第 \(i-1\) 位均为 \(0\) 的点集的二进制表示),注意加上 \(i\to (0,0)\) 。
复杂度 \(O(n^2 \times 2^n)\)
由此题可以看出,状压DP由于需要设 \(2^n\) 种状态,所以 \(n\) 的大小通常不会很大
这个特点在矩阵乘法加速的复习中也有提到。根据不同的需要选择即可。
矩乘主要是用于加速递推,而状压主要是在DP原来的 “只存最优值和关键值” 的基础上,需要知道具体状态的一种方式。两种差别还是很大的。
代码
#include <bits/stdc++.h>
#define lb double
using namespace std;
const int N=21;
lb x[N],y[N],f[N][35000];
int n;
lb get_dis( int a,int b )
{
return sqrt( (x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b]));
}
int main()
{
scanf( "%d",&n );
for ( int i=1; i<=n; i++ )
scanf( "%lf%lf",&x[i],&y[i] );
memset( f,127,sizeof(f) );
for ( int s=1; s<=(1<<n)-1; s++ ) //枚举集合
for ( int i=1; i<=n; i++ )
{
if ( (s&(1<<(i-1)))==0 ) continue; //根本就没经过,不可能是起点
if ( s==(1<<i-1) ) { f[i][s]=0; continue;}
for ( int j=1; j<=n; j++ )
{
if ( (s&(1<<(j-1)))==0 || i==j ) continue;
f[i][s]=min( f[i][s],f[j][s-(1<<(i-1))]+get_dis(i,j) );
}
}
lb ans=-1;
for ( int i=1; i<=n; i++ )
{
lb s=f[i][(1<<n)-1]+get_dis(i,0); //加上到起点的距离,不过放在初始化应该也没问题
if ( ans==-1 || ans>s ) ans=s;
}
printf( "%.2lf",ans );
return 0;
}
1——P5911 [POI2004]PRZ
题目链接 luogu
题意
给定桥能承受的最大重量 \(W\) ,队员体重 \(w_i\) ,队员通过时间 \(t_i\) ,每一组通过的时间为最慢的一个。问最小总时间。
思路
非常典型的一道枚举子集状压。
重点在于枚举子集的代码:
for ( int S1=S; S1!=0; S1=(S1-1)&S ) //S1是子集,S是总集,S2是S1对于S的补集
S2=S^S1;
考虑如何证明这样枚举了所有的子集。
首先,对于 \(x<y\) ,显然有 \(x\and y \in y\) . 那么可以知道,\((S1-1)\and S\) 一定是 \(S\) 的子集。
然后,对于 \(S1-1\) ,它显然是第一个比 \(S1\) 小的集合,然后对 \(S\) 做 $\and $ 操作,那么就是 “比 \(S1\) 小的第一个 \(S\) 的子集”,这样可以保证枚举完所有的子集。
或者,先考虑特殊情况,即 \(S=2^i\) 。这个时候,显然有子集 \(S1=S1-1\) 这样的操作可以枚举子集。那么现在也是一样,首先同样地 \(-1\) ,然后 \(\and S\) 保证是 \(S\) 的子集,在 \(S=2^i\) 的情况下比 \(S1\) 小的第一个子集去掉不合法的1,显然不会比其他的子集更小。
然后对于这道题,预处理每种状态(单独成一组过桥),然后状压即可。
#include <bits/stdc++.h>
using namespace std;
const int N=(1<<16)+1,inf=0x3f3f3f3f;
int W,n,w[21],t[21];
int ti[N],wi[N],f[N];
void init()
{
for ( int i=0; i<(1<<n); i++ )
{
int dig=i,cnt=0;
while ( dig )
{
cnt++;
if ( dig&1 ) wi[i]+=w[cnt],ti[i]=max( ti[i],t[cnt] );
dig>>=1;
}
f[i]=inf;
}
}
int main()
{
scanf( "%d%d",&W,&n );
for ( int i=1; i<=n; i++ )
scanf( "%d%d",&t[i],&w[i] );
init(); f[0]=0;
for ( int i=0; i<(1<<n); i++ )
for ( int j=i; j>=0; j=i&(j-1) )
{
if ( wi[i^j]<=W ) f[i]=min( f[i],f[j]+ti[i^j] );
if ( j==0 ) break;
}
printf( "%d",f[(1<<n)-1] );
return 0;
}
2——P1879 [USACO06NOV]Corn Fields G
题目链接 luogu
题意
一块长方形的牧场,M行N列,每一格都是一块正方形的土地,在上面种草。有些土地相当贫瘠,不能用来种草。没有哪两块草地有公共边。求一共有多少种种植方案。
思路
考虑不同行之间的转移(显然,直接存下 \(144\) 位的二进制并不现实),\(f[i][j]\) 为当前行 \(i\) ,状态为 \(j\) 。
每次转移的时候判断:和被转移的上一行状态是否冲突;这一行本身是否冲突。显然这两个都可以预处理掉。最后,由于每次由上一行转移过来,所以要预先求出第一行的状态(没有上一行的限制,只需要算本行不能放的即可。)
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=4100,mod=1e8;
int f[15][N],lin[N],pd[N],mp[15][15],n,m;
int main()
{
scanf( "%d%d",&m,&n );
int num=(1<<n)-1;
for ( int i=1; i<=m; i++ )
for ( int j=1; j<=n; j++ )
scanf( "%d",&mp[i][j] );
for ( int i=1; i<=m; i++ )
for ( int j=1; j<=n; j++ )
lin[i]=(lin[i]<<1)+(mp[i][j] ? 0 : 1);
//当前行不能放
int cnt=0;
for ( int i=0; i<=num; i++ )
{
if ( i&(i>>1) ) continue;
pd[++cnt]=i; //确保同行的不相邻
}
int now,las;
for ( int i=1; i<=cnt; i++ ) //第一行init
{
now=pd[i];
if ( pd[i]&lin[1] ) continue;
f[1][i]=1;
}
for ( int i=2; i<=m; i++ )
for ( int j=1; j<=cnt; j++ )
{
las=pd[j]; //上一行
if ( las&lin[i-1] ) continue; //上一行不行
for ( int k=1; k<=cnt; k++ )
{
now=pd[k];
if ( now&lin[i] ) continue; //本身不行
if ( now&las ) continue; //和上一行冲突
(f[i][k]+=f[i-1][j])%=mod;
}
}
int ans=0;
for ( int i=1; i<=cnt; i++ )
(ans+=f[m][i])%=mod;
printf( "%d",ans );
return 0;
}
3——P3052 [USACO12MAR]Cows in a Skyscraper G
题目链接 luogu
题意
给出 \(n\) 个物品,体积为 \(w[i]\),现把其分成若干组,要求每组总体积 \(<=W\),问最小分组数。\(n<=18\)
思路
状压显然。用 \(f[i]\) 记录当前状态为 \(i\) 时,所需要的最小组数。\(v[i]\) 表示状态 \(i\) 下,没满的那一组剩余体积。转移时判断 \(v[i]\) 和 \(a[j]\) 的大小即可。
注意此题有一些细节,见代码注释。
#include <bits/stdc++.h>
using namespace std;
const int N=21,M=(1<<N);
int n,W,f[M],v[M],a[N];
int main()
{
scanf( "%d%d",&n,&W );
for ( int i=1; i<=n; i++ )
scanf( "%d",&a[i] );
memset( f,0x3f,sizeof(f) );
f[0]=1; v[0]=W;
for ( int i=0; i<(1<<n); i++ )
for ( int j=1; j<=n; j++ )
{
if ( i&(1<<j-1) ) continue;
if ( a[j]<=v[i] && f[i|(1<<j-1)]>f[i] )
{
f[i|(1<<j-1)]=f[i];
v[i|(1<<j-1)]=v[i]-a[j];
}
if ( a[j]<=v[i] && f[i|(1<<j-1)]==f[i] )
v[i|(1<<j-1)]=max( v[i|(1<<j-1)],v[i]-a[j] );
//一开始没加这个特判……不仅是f[i]需要取最优,在f[i]相同的情况下,
//剩余空间越大显然也更优。
if ( a[j]>v[i] && f[i|(1<<j-1)]>=f[i]+1 )
{
f[i|(1<<j-1)]=f[i]+1;
v[i|(1<<j-1)]=max( v[i|(1<<j-1)],W-a[j] );
//新建了一组,但是至于“当前组”选哪一组是可以max的
//因为上一组也没放满
}
}
printf( "%d",f[(1<<n)-1] );
return 0;
}
4——P5640 【CSGRound2】逐梦者的初心
题目链接 luogu
题意
给你一个长度为 \(n\) 的字符串 \(S\)。有 \(m\) 个操作,保证 \(m\le n\) 。你还有一个字符串 \(T\),刚开始为空。共有两种操作。
第一种操作:在字符串 \(T\) 的末尾加上一个字符。
第二种操作:在字符串 \(T\) 的开头加上一个字符。
每次操作完成后要求输出有几个 \(l \in [1,T.size]\) 满足以下条件:
对于 \(\forall i \in [1,l]\) 有 \(T_{T.size-l+i} \ne S_{i}\)
Tip:字符串下标从 \(1\) 开始。\(T.size\) 表示 \(T\) 的长度。
思路
由于每个位置和每种字符都是独立的,对每种字符都记录一下位子,用 \(f [i]=0/1\) 表示长度为 \(i\) 的后缀是否合法,\(0\) 表示可以,\(1\) 表示不行。于是bitset优化就比较明显了。
在末尾加一个字符时,左移一位做 or 运算,因为状态由 \(i-1\) 的情况加上新的一位转移过来。
在开头加一个字符时,直接or上该字符出现的状态左移长度减一位,因为开头加入字符,那么后面的所有字符后移一位。这里需要费用提前的思想,即每次加入字符时,将所有能贡献到答案的位置一次存好。考虑如何进行。设 \(ch\) 在第 \(i\) 位加入,那么离末尾的长度是 \(i-1\) .设 \(ch\) 对应的 \(id\) 在第 \(k\) 位是1,那么取到第 \(k\) 位一定不合法,这时候 \(ch\) 的位置由队尾左移 \(i-1\) 位得到,所以当 \(ch\to k\) 的时候,\(tail\to k-l+1\) ,由于存储是倒序的,所以有 f=f|(id[ch]<<(i-1));
答案就是范围内 \(0\) 的个数。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m,opt,s[N],ch;
bitset<35010> f,id[1010],now;
int main()
{
scanf( "%d%d",&n,&m );
for ( int i=1; i<=n; i++ )
scanf( "%d",&s[i] );
for ( int i=1; i<=m; i++ )
id[s[i]].set(i);
now.set();
for ( int i=1; i<=m; i++ )
{
scanf( "%d%d",&opt,&ch );
now.reset(i);
if ( opt==0 ) f=(f<<1)|id[ch];
else f=f|(id[ch]<<(i-1));
printf( "%d\n",(~(f|now)).count() );
}
return 0;
}
5——P2704 [NOI2001]炮兵阵地
题意
一个 \(N\times M\) 的地图,每一格是山地(H
)或平原(P
). 每一格平原地形上可以布置一支炮兵部队(山地上不能部署).如果在地图中的平原上部署一支炮兵部队,则它能攻击到的区域:沿横向左右各两格,沿纵向上下各两格。攻击范围不受地形的影响。 现在,保证任何一支炮兵部队都不在其他支炮兵部队的攻击范围内,求最多能放的炮兵数量。 \(N\leq 100,M\leq 10\)
思路
一个很显然的想法是,既然炮兵的攻击范围是两行,那么每个 \(dp\) 状态就放当前行,上面两行就好了。不幸的是,由于 \(N\) 比较大,这样开状态就 MLE了。所以考虑如何得知上两行的状态。然后就会发现,如果状态只存储当前行号 \(x\),当前状态 \(i\) ,以及上一行的状态 \(j\) ,那么在转移的时候通过 \(j\) 可以得到 \(x-1\) 行的“当前状态”,而 \(x-2\) 行的 “当前状态” 可以通过 \(x-1\) 行的 “上一行状态” 得到,那么问题就解决了,只需要存两个状态。但是开整个地图显然不现实,所以还需要加滚动数组,也就是只存储当前行、上一行的状态,每次 \(\mod 2\) 即可。这样就完整地解决了空间问题。
用 \(f[L][S][i]\) 表示当前是第 \(i\) 行,这一行状态是 \(S\) ,上一行状态是 \(L\) ,那么
\]
\(sum[S]\) 表示 \(S\) 这个状态 \(1\) 的个数(即炮兵的个数)
实现时注意细节,上文中并没有考虑地形是否合法的问题,这个可以预处理出来。还有每一行之内的合法状态,每一个合法状态对应的 \(sum\) 都需要预处理,这样可以只枚举合法状态,大大减少循环次数。
#include <bits/stdc++.h>
using namespace std;
const int N=110,M=11,S=1<<M;
int n,m,g[N],f[2][S][S],cnt[S];
vector<int> state;
bool check( int s )
{
for ( int i=0; i<m; i++ )
if ( (s>>i&1) && ( (s>>i+1&1) || (s>>i+2&1) ) ) return 0;
return 1;
}
int count( int s )
{
int res=0;
for ( ; s; s>>=1 )
res+=(s&1);
return res;
}
int main()
{
scanf( "%d%d",&n,&m );
for ( int i=0; i<n; i++ )
for ( int j=0; j<m; j++ )
{
char ch; cin>>ch;
if ( ch=='H' ) g[i]+=(1<<j);
}
for ( int i=0; i<(1<<m); i++ )
if ( check(i) ) state.push_back(i),cnt[i]=count(i);
for ( int i=0; i<=n+1; i++ )
for ( int j=0; j<state.size(); j++ )
for ( int k=0; k<state.size(); k++ )
for ( int u=0; u<state.size(); u++ )
{
int t1=state[u],t2=state[j],t3=state[k];
if ( (t1&t2) || (t1&t3) || (t2&t3) ) continue;
if ( g[i]&t3 ) continue;
f[i&1][j][k]=max( f[i&1][j][k],f[i-1&1][u][j]+cnt[t3] );
}
printf( "%d",f[n+1&1][0][0] );
}
6——P3959 宝藏
题意
藏宝图上标出了 \(n\) 个宝藏屋, 和这 \(n\) 个宝藏屋之间可供开发的 \(m\) 条道路和它们的长度,起点任意。
已有道路可以0代价通行,每开凿一条新的道路,就会挖掘出该道路所能到达的宝藏屋的宝藏,不会开发两个已经被挖掘的屋子之间的道路。新开发一条道路的代价是:
\]
\(L\) 代表这条道路的长度,\(K\) 代表从最初的起点到这条道路的起点所经过的宝藏屋的数量(包括头尾) 。
求工程总代价最小值。
思路
状压,状态存储:已经连通的点集。
令 \(f[i][j][k]\) 表示状态为 \(j\) ,深度为 \(k\) (第 k 层),第 \(i\) 个点加入的代价。(\(k\) 的加入是为了防止做到后面的时候前面的层还没有更新,避免了深度混乱的情况)
然后考虑记忆化搜索。参数:当前状态 \(now\) ,总代价 \(sum\) ,深度 \(dep\) ,每次用当前状态中已经有的点更新其他的点并加入状态。答案就是点集 \(=U\) 的最小代价。
细节:需要加一些剪枝,比如当前解比当前最优解大了直接return,因为之后的解只增不减。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=15,inf=1e9+7;
int n,m,lim,ans=inf,mp[N][N],dis[N],f[N][5010][N];
void dfs( int now,int sum,int dep )
{
if ( sum>=ans ) return;
if ( now==lim ) { ans=min( ans,sum ); return; }
for ( int i=1; i<=n; i++ )
if ( 1<<(i-1)&now )
for ( int j=1; j<=n; j++ )
if ( !(1<<(j-1)&now) && mp[i][j]<inf )
{
if ( f[j][1<<(j-1)|now][dep+1]<=sum+dis[i]*mp[i][j] ) continue;
f[j][1<<(j-1)|now][dep+1]=sum+dis[i]*mp[i][j];
dis[j]=dis[i]+1;
dfs( 1<<(j-1)|now,f[j][1<<(j-1)|now][dep+1],dep+1 );
}
}
int main()
{
scanf( "%d%d",&n,&m );
lim=(1<<n)-1; memset( mp,inf,sizeof(mp) );
for ( int i=1,u,v,w; i<=m; i++ )
{
scanf( "%d%d%d",&u,&v,&w );
mp[u][v]=mp[v][u]=min( mp[u][v],w );
}
for ( int i=1; i<=n; i++ )
{
memset( dis,0,sizeof(dis) ); memset( f,inf,sizeof(f) );
dis[i]=1; dfs( 1<<(i-1),0,0 );
}
printf( "%d",ans );
return 0;
}
7——P1357 花园
题目链接 luogu
题意
有一座环形花园,顺时针方向编号为 \(1 \sim n\) 。花园 \(1\) 和 \(n\) 相邻。任意相邻 \(m\) 个花圃中都只有不超过 \(k\) 个 C
形的花圃,其余花圃均为 P
形的花圃。求出符合规则的花园种数对 \(1e9+7\) 取模的结果。
$ 2 \leq n \le 10^{15},2 \leq m \leq \min(n, 5)$
思路
(以上数据范围为 \(100\) )如果只是 80 分做法,那么显然,根据 \(m\leq 5\) 的范围,可以想到状压(因为只有两种状态,为 c
或是为 P
),特别处理环形即可。
现在考虑如何从 \(n\leq 1e5\to n\leq 1e{15}\) .对于这个范围,首先盲猜一个可以接受的复杂度,必须是 \(O(\sqrt n)\) 或是 \(O(\log n)\) 。根号做法一般都是分块之类的方法,在 状压DP 上能优化的可能性不大;那么考虑是否存在一个 \(\log n\) 做法。看到 \(\log n+DP\) ,首先会想到矩乘加速。
以上是口胡的大致思路,现在来具体分析整个 DP 方程和转移 base。
首先考虑简单状压。对于一个新的花园,受到之前 \(m-1\) 个花园的样式限制,那么状态就是 \(S\) ,表示当前 \(m\) 个连续花园的样式情况。每次转移的时候相当于在原来的状态后面加了一位,令 \(dp[i][s]\) 表示目前在第 \(i\) 个花园,左侧 \(m\) 个状态为 \(S\) 的方案数。那么方程就是:
\]
dfs 预处理出所有可能状态。然后处理环形。令 \(dp[m][S]=1\) ,然后转移到 \(dp[n+m]\) ,把 \(dp[n+m][S]\) 累加到答案。因为前 \(m\) 个花圃等同于 \(n+1\to m.\)
然后考虑如何得到转移矩阵 \(base.\)
首先,浪费一层循环以化简这个式子:
\]
但是这样还要考虑状态能否转移的问题。所以不如再来一个数组 \(pd[k][j]\) 表示状态 \(k\) 能否转移到 \(j\).改写式子:
\]
然后就愉快地发现,这是个标准的矩乘式子,连推都不需要就得到了一个现成的 \(base.\)
然而还没有结束!我们还没有得到最终的答案。继续思考,我们最终的答案是对于每一个状态 \(S\) ,取 \(dp[n+m][S]\) ,也就是说,是 \(dp\) 矩乘上 \(pd\) 第 \(S\) 列的和。把 \(dp\) 数组缩减到一维,并且做矩阵乘法,那么就会发现,最终结果等同于乘上 \(pd\) 的对角线部分。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=1e5+10,mod=1e9+7;
ll n,m,K,status[40],cnt=0,vis[40];
struct matrix
{
ll mt[40][40];
matrix () { memset( mt,0,sizeof(mt) ); }
void reset()
{
memset( mt,0,sizeof(mt) );
for ( ll i=0; i<=32; i++ )
mt[i][i]=1;
}
matrix operator * ( const matrix b )
{
matrix c;
for ( ll i=0; i<=32; i++ )
for ( ll j=0; j<=32; j++ )
for ( ll k=0; k<=32; k++ )
c.mt[i][j]=(c.mt[i][j]+mt[i][k]*b.mt[k][j])%mod;
return c;
}
}ans,base;
bool check( ll s )
{
ll res=0;
for ( ; s; s>>=1 )
if ( s&1 ) res++;
return res<=K;
}
void dfs( ll x,ll S )
{
if ( x==m )
{
if ( check(S) )
{
status[++cnt]=S; base.mt[(S>>1)+(1<<(m-1))][S]=1;
base.mt[S>>1][S]=1; vis[S]=1;
}
return;
}
dfs( x+1,S ); dfs( x+1,S+(1<<x) );
}
matrix power( matrix a,ll b )
{
matrix res; res.reset();
for ( ; b; b>>=1,a=a*a )
if ( b&1 ) res=res*a;
return res;
}
signed main()
{
scanf( "%lld%lld%lld",&n,&m,&K );
dfs( 0,0 ); ans=power( base,n );
ll sum=0;
for ( ll i=0; i<=32; i++ )
if ( vis[i] ) (sum+=ans.mt[i][i])%=mod;
printf( "%lld",sum );
}
Last
To be continue…… Maybe.