因为篇幅太长翻着麻烦,计划把DP拆成几个小专题,这里原文只留下状压,其他请至后续博文。

状态压缩优化

所谓状态压缩,就是将原本需要很多很多维来描述,甚至暴力根本描述不清的状态压缩成一维来描述。

时间复杂度一般为\(O(2^n\cdot n^2)\)的形式

ZZ并不太会算复杂度,如果博客中复杂度有错误,请指出并尽情嘲讽我,谢谢!)

眼界极窄的ZZ之前只是听说过这个名字……先感谢Lrefrain学长把这个东西介绍给我orz

使用状态压缩优化的常见情景:

  • 这个数据范围怎么有一维出奇的小啊?

互不侵犯

应该是最经典的一道状压dp了,看到这极具特色的数据范围就会了大半

可以对每一行可能出现的所有状态进行压缩,因为每一个位置不是放就是不放,所以我们把放标成1,不放标成0,那么对于一行来说,每种状态都可以用一个二进制串来表示,好妙啊!!!

更妙的是,既然用了二进制,那么就可以使用位运算的<<>>&运算符,直接判定相邻两行的状态合不合法!

这个真的需要好好体会,越体会越妙!

放一下代码吧,但更重要的是领会精神!

#include<bits/stdc++.h>
using namespace std;
#define MA 1005
#define ll long long ll sit[MA]={0},ku[MA]={0};
ll cnt=0;
ll n,k;
ll dp[15][MA][100]={0}; int main()
{
scanf("%lld%lld",&n,&k);
for(int i=0;i<(1<<n);i++)
{
if(i&(i<<1))continue;
sit[++cnt]=i;
for(int j=0;j<n;j++)
{
if(i&(1<<j))ku[cnt]++;
}
}
dp[0][1][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=cnt;j++)
{
for(int p=ku[j];p<=k;p++)
{
for(int q=1;q<=cnt;q++)
{
if(sit[j]&sit[q])continue;
if(sit[j]&(sit[q]>>1))continue;
if(sit[j]&(sit[q]<<1))continue;
dp[i][j][p]+=dp[i-1][q][p-ku[j]];
}
}
}
}
ll ans=0;
for(int i=1;i<=cnt;i++)
{
ans+=dp[n][i][k];
}
printf("%lld",ans);
return 0;
}

一个来自学长的小技巧

用\(lowbit\)或者枚举算\(01\)串中\(1\)的个数时,如果要算的串很多,可能导致此处算法复杂度爆炸而被卡

那么这个时候,就可以用预处理的方式,先算出所有状态的\(1\)个数,用到的时候直接\(O(1)\)查询就行~

炮兵阵地

发现还要考虑上下,这怎么办呢?

首先,其实考虑下面就相当于下面的考虑上面,所以就不用考虑下面了(有点绕)

然后,因为上面只需要伸两格,所以直接用两维表示\(i\),\(i-1\)行的状态,每次由\(i-1\),\(i-2\)转移得到就可以了!

题里还需要考虑一个平原的问题,这里我们把每行的地形也压缩一下,在进行dp的时候注意判定状态是否合法就行啦~\(≧▽≦)/~

dp部分(其实写得有些麻烦了,不过看起来很整齐)

	for(int i=1;i<=cnt;i++)
{
if(sit[i]&pa[1])continue;
dp[1][i][0]=max(dp[1][i][0],mu[i]);
}
for(int i=1;i<=cnt;i++)
{
if(sit[i]&pa[1])continue;
for(int j=1;j<=cnt;j++)
{
if(sit[j]&pa[2])continue;
if(sit[j]&sit[i])continue;
dp[2][j][i]=max(dp[2][j][i],mu[i]+mu[j]);
}
}
for(int i=3;i<=n;i++)
{
for(int j=1;j<=cnt;j++)
{
if(sit[j]&pa[i])continue;
for(int k=1;k<=cnt;k++)
{
dp[i%3][j][k]=0;
if(sit[k]&pa[i-1])continue;
if(sit[j]&sit[k])continue;
for(int l=1;l<=cnt;l++)
{
if(sit[l]&pa[i-2])continue;
if(sit[j]&sit[l])continue;
if(sit[k]&sit[l])continue; dp[i%3][j][k]=max(dp[i%3][j][k],dp[(i-1)%3][k][l]+mu[j]);
}
}
}
}

特殊方格棋盘

在有前两道题的基础后,这道题应该是不难的一道题,放在后面是因为做的时候发现有一种解法(自我感觉)比较优美

观察整个题,发现每行能且只能放一个车,这就说明不可能在不同的两行上出现相同的状态,也就是在棋盘上每行每一种状态都是不同的

所以我想,既然全都不同,能不能不维护行数,把空间降一维呢?经过思考,这是可以实现的。

我们用\(sit[p]\)来表示某行的一种状态,那么这种状态中随便删去一个1,就能得到上一行的一个状态,我们只要枚举每一个1,然后用删去他得到的上一行某个状态的答案更新这一行的答案即可。这里的删去是可以用异或运算轻松实现的,真的是妙!

通过算1的个数,我们得到行号,我们枚举到的1的位置+1便可以得到列号,如果这个坐标合法,我们就更新答案。(注意这个+1!)

当时的代码:(可能一些细节和以上说的略有不同,但是思路是一样的)

#include<bits/stdc++.h>
using namespace std;
#define ll long long ll n,m;
ll dp[1<<21]={0};
ll a[25][25]={0};
ll x,y;
ll z[1<<21]={0}; int main()
{
for(int i=1;i<=(1<<21);i++)
{
z[i]=z[i>>1]+(i&1);
}
scanf("%lld%lld",&n,&m);
while(m--)
{
scanf("%lld%lld",&x,&y);
a[x][y-1]=1;
}
dp[0]=1;
for(int i=1;i<(1<<n);i++)
{
for(int j=0;j<n;j++)
{
if((i&(1<<j))&&!a[z[i]][j])
{
dp[i]+=dp[i^(1<<j)];
}
}
}
printf("%lld",dp[(1<<n)-1]);
return 0;
}

Disease Manangement 疾病管理

感性理解一下:就是把所有选中的牛得的病全部叠起来,然后统计\(1\)不超过\(K\)的数量

这题可以只用一维的,不知为何写二维反而没过……

问题不大,代码略,重头戏都在后面了……

旅游景点 Tourist Attractions

噩 梦 的 开 端(对于我来说

其实做完想想这题并不很难……然而由于我的\(sb\)行为\(dijkstra\)写挂了???导致颓了题解才幡然醒悟,我真是优秀啊。。。

然后就是我的程序自带大常数……所以以后遇到要重复计算的最好压到函数里吧。

吐槽完了,理一下思路吧,首先题中在某个城市停留是需要经过某个特定城市的,因为\(k\)的范围很小,可以考虑把停留过城市的情况压成一维。

那么再把停留时的限制压缩成一个数组\(lim_{1\dots k}\),判定是否合法时我们把上一步的状态和这个\(lim\)取一下&,如果结果仍然等于上一步的状态就意味着这个\(lim\)已经包含在上一步的状态中,也就是已经被满足了。

然后我们跑最短路,对每一个要停留的点跑一边\(dijkstra\),得到他们之间的最短路(注意,这里需要单独存一下每个点到\(n\)的最短路)

然后就是一波状压DP正常流程

最后统计答案,取一个\((dp_{(1<<k)-1,i}+xdis_{i,0})_{min}\)(这里\(dp_{(1<<k)-1,i}\) 指最后一步到达\(i\)状态为所有都已经停留的最短路,\(xdis_{i,0}\)指每个点到\(n\)的距离。

放一段核心的吧……

        dp[0][1]=0;
for(reg int i=0;i<=all;i++)
{
for(reg int j=1;j<=k+1;j++)
{
if(dp[i][j]!=inf)
{
for(reg int l=2;l<=k+1;l++)
{
if((i&lim[l])==lim[l])
{
dp[i|(1<<(l-2))][l]=min(dp[i|(1<<(l-2))][l],dp[i][j]+xdis[j][l]);
}
}
}
}
}
ans=inf;
for(reg int i=1;i<=k+1;i++)ans=min(ans,dp[(1<<k)-1][i]+xdis[i][0]);
cout<<ans;

补一个\(dijkstra\)板子防降智\(qaq\)

priority_queue<node> h;
inline void dijkstra(ll s)
{
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
h.push((node){s,0});
while(!h.empty())
{
node ta=h.top();
ll t=ta.no;
h.pop();
if(vis[t])continue;
vis[t]=1;
for(reg int i=head[t];i;i=next[i])
{
if(dis[e[i].v]>dis[t]+e[i].w)
{
dis[e[i].v]=dis[t]+e[i].w;
h.push((node){e[i].v,dis[e[i].v]});
}
}
}
}

总之永远不要放弃啊,你还会面对更多的挑战呢

愤怒的小鸟

强行解个解析式,然后直接状压就完事了。

首先三点是可以确定一条二次函数曲线的,所以对于每两头猪,如果他们的\(x\)不等,一定可以有一只从原点发射的鸟能够同时击杀,因此我们枚举每一对猪,求出击杀他们的二次函数曲线解析式(也就是求\(a,b\))

简单写下过程:

设猪的坐标为\((x_1,y_1),(x_2,y_2)\),则有

\(\begin{cases}
ax_1^2+bx_1=y_1\\
ax_2^2+bx_2=y_2
\end{cases}\)

初中生感到亲切

这里的\(x_1,y_1,x_2,y_2\)都是已知的,暴力求解,得到

\(\begin{cases}
a=\frac{x_2y_1-x_1y_2}{x_1^2x_2-x_2^2x_1}\\
b=\frac{y_1x_2^2-y_2x_1^2}{x_1x_2^2-x_2x_1^2}
\end{cases}
\)

得到许多解析式以后,因为可能存在一条曲线过多只猪,所以我们要记得把所有满足条件的猪都加入到这条曲线的状态中,另外还要考虑只射一只猪的情况,为其他鸟留下表演空间。

还有一个很重要的优化,就是显然每个状态的二进制表示中没射过的最后一只猪早晚都是要射的,所以不如直接枚举包含最后一只猪的状态,因为考虑过只射一只,所以这样没有后效性,我们预处理一下每种状态最后一只没被射的猪的编号就行

\(code:\)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld double
const double eps=1e-6;
#define N 20 void cal(ld &a,ld &b,ld x1,ld y1,ld x2,ld y2)
{
b=(y2*x1*x1-y1*x2*x2)/(x2*x1*x1-x1*x2*x2);
a=(y1-x1*b)/(x1*x1);
} inline ld func(ld a,ld b,ld x)
{
return a*x*x+b*x;
} ll lowzer[1<<N]={0}; inline void getlow()
{
for(int i=0;i<(1<<N);i++)
{
ll j=0;
while(i&(1<<j)&&j<=18)j++;
lowzer[i]=j+1;
}
} ll bird[N][N]={0};
ld pos[N][3]={0};
ll dp[1<<N]={0};
ll t,n,m;
ld a,b; void clean()
{
memset(bird,0,sizeof(bird));
memset(pos,0,sizeof(pos));
memset(dp,0x3f3f3f3f,sizeof(dp));
} inline bool dif(ld a,ld b)
{
//cout<<"\t\t"<<a<<' '<<b<<' '<<eps<<endl;
return !(fabs(a-b)<=eps);
} int main()
{
cin>>t;
getlow();
while(t--)
{
clean();
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>pos[i][1]>>pos[i][2];
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(!dif(pos[i][1],pos[j][1]))continue;
cal(a,b,pos[i][1],pos[i][2],pos[j][1],pos[j][2]);
if(a>-eps)continue;
//cout<<'\t'<<a<<' '<<b<<endl;
for(int k=1;k<=n;k++)
{
if(!dif(func(a,b,pos[k][1]),pos[k][2]))
{
bird[i][j]|=(1<<(k-1));
//cout<<bird[i][j]<<endl;
}
}
}
}
dp[0]=0;
for(int i=0;i<(1<<n);i++)//before
{
ll j=lowzer[i];
for(int k=1;k<=n;k++)//after
{
dp[i|bird[j][k]]=min(dp[i|bird[j][k]],dp[i]+1);
}
dp[i|(1<<(j-1))]=min(dp[i|(1<<(j-1))],dp[i]+1);
}
cout<<dp[(1<<n)-1]<<endl;
}
return 0;
}

(教训:一定要看清楚自己写的函数的传参顺序)

动物园

看到数据范围,似乎并没有小的数?

哦他只能看五格……那就存五格的状态好了

设\(dp[i][S]\)为到前\(i\)格,现在看到状态为\(S\)时开心小朋友的最大值

在转移的时候先取当前状态的后四位,左移一格再讨论之前的一格是\(0\)是\(1\),转移就很好写了:

dp[j][s]=max(dp[j-1][(s&15)<<1],dp[j-1][(s&15)<<1|1])+cnt[j][s];

其中,\(cnt[j][S]\)这个数组维护的是站在第\(j\)格状态为\(S\)能开心的小朋友总数,这个数组我们读入以后预处理一下就行

另外因为是一个环所以我们先钦定一下最后一项的状态,然后把\(dp[0][S_0]\)设成\(0\),其他都设\(-inf\),最后\(dp[n][S_0]\)就是答案

不多说了,重在理解。

\(code:\)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N 10005
#define C 50005 ll n,c;
ll e,f,l,tmp;
ll lik[C],hte[C];
ll cnt[N][32];
ll dp[N][32]; inline ll r()
{
ll s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')w=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
s=s*10+ch-'0';
ch=getchar();
}
return s*w;
} int main()
{
cin>>n>>c;
for(int i=1;i<=c;i++)
{
e=r();f=r();l=r();
for(int j=1;j<=f;j++)
{
tmp=r();
hte[i]|=(1<<((tmp-e+n)%n));
}
for(int j=1;j<=l;j++)
{
tmp=r();
lik[i]|=(1<<((tmp-e+n)%n));
} for(int j=0;j<32;j++)
{
cnt[e][j]+=(bool)((j&hte[i])||(~j&lik[i]));
}
}
ll ans=0;
for(int i=0;i<32;++i)
{
memset(dp[0],128,sizeof(dp[0]));
dp[0][i]=0;
for(int j=1;j<=n;++j)
{
for(int s=0;s<32;++s)
{
dp[j][s]=max(dp[j-1][(s&15)<<1],dp[j-1][(s&15)<<1|1])+cnt[j][s];
}
}
ans=max(ans,dp[n][i]);
}
cout<<ans;
return 0;
}

排列perm

因为长度比较小,所以设状态为当前是否选了某位,另一维设成当前的余数。

设\(dp[i][S]\)为当前组成余数为\(i\),状态为\(S\)的方案总数,用数组记录某位置的数是否选过(用于去重),统计一下就好,答案是\(dp[0][(1<<len)-1]\)。

主要部分:

                for(r int i=0;i<(1<<len)-1;i++)
{
for(r int j=0;j<d;j++)
{
memset(used,0,sizeof(used));
for(r int k=0;k<len;k++)
{
if((i|(1<<k))!=i&&!used[num[k]])
{
used[num[k]]=1;
dp[i|(1<<k)][(j*10+num[k])%d]+=dp[i][j];
}
}
}
}

集合选数

刚开始试图推结论……然而并没推出来

然后突然发现其实以每一个\(i%2||i%3\)的数为左上角建立一个矩阵,使得

\(\begin{cases}
a_{i,j}=a_{i,j-1}*3\\
a_{i,j}=a_{i-1,j}*2
\end{cases}
\)

那么一个数肯定不能和他上下左右的数被同时取到,于是就转化成状压的经典题型,设\(dp[i][S]\)为第\(i\)行状态为\(S\)的答案总数,其他就是基本操作了

不过要注意一点,就是虽然注意了要清空,但是不要清空太多次……毕竟\(memset()\)似乎也是\(O(n)\)的,你要是每次都清空直接给自己平方可还行。所以每次建矩阵前清空下就成

这题最精华的部分应该就在于想到构造矩阵了吧,其他思路过去以后就不难了,而且我码风太丑所以就不放代码了

Bill的挑战

字符串数量不多,所以我们按列考虑,然后用每列的匹配状态转移。

因为他要\(k\)个字符串,所以预处理一下每个状态里\(1\)的个数

设\(dp[i][S]\)为匹配到第\(i\)位已匹配状态为\(S\)的方案数

然后枚举用于匹配字符串的每一位和上一位,因为有一位不行就无法匹配,所以转移时要用&

感觉自己讲的好乱……请看代码辅助理解吧(也不知道有没有人会看)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MOD 1000003
#define reg register ll t;
ll n,k;
char ch[17][55];
ll len;
ll pos[55][1<<16];
ll dp[55][1<<16];
ll cnt[1<<16]; inline void clean()
{
memset(dp,0,sizeof(dp));
memset(pos,0,sizeof(pos));
memset(ch,0,sizeof(ch));
dp[0][(1<<n)-1]=1;
} inline void pre()
{
for(int i=1;i<(1<<16);i++)cnt[i]=cnt[i>>1]+(i&1);
} int main()
{
scanf("%lld",&t);
pre();
while(t--)
{
scanf("%lld%lld",&n,&k);
clean();
for(int i=0;i<n;i++)scanf("%s",ch[i]+1);
len=strlen(ch[0]+1);
for(reg int i=0;i<n;i++)
{
for(reg int j=1;j<=len;j++)
{
for(reg int k=0;k<26;k++)
{
if(ch[i][j]-'a'==k||ch[i][j]=='?')
{
pos[j-1][k]|=(1<<i);
}
}
}
}
for(reg int j=1;j<=len;j++)//before
{
for(reg int i=0;i<(1<<n);i++)
{
if(dp[j-1][i])for(reg int k=0;k<26;k++)
{
dp[j][i&pos[j-1][k]]=(dp[j][i&pos[j-1][k]]+dp[j-1][i])%MOD;
}
}
}
ll ans=0;
for(reg int i=0;i<(1<<n);i++)
{
if(cnt[i]==k)ans=(ans+dp[len][i])%MOD;
}
printf("%lld\n",ans);
}
return 0;
}

小星星

给神题留坑。

【刷题笔记】DP优化-状压的更多相关文章

  1. 「刷题笔记」DP优化-状压-EX

    棋盘 需要注意的几点: 题面编号都是从0开始的,所以第1行实际指的是中间那行 对\(2^{32}\)取模,其实就是\(unsigned\ int\),直接自然溢出啥事没有 棋子攻击范围不会旋转 首先, ...

  2. 刷题总结——bzoj1725(状压dp)

    题目: 题目描述 Farmer John 新买了一块长方形的牧场,这块牧场被划分成 N 行 M 列(1<=M<=12; 1<=N<=12),每一格都是一块正方形的土地. FJ  ...

  3. LeetCode刷题笔记-DP算法-取数问题

    题目描述 (除数博弈论)爱丽丝和鲍勃一起玩游戏,他们轮流行动.爱丽丝先手开局. 最初,黑板上有一个数字 N .在每个玩家的回合,玩家需要执行以下操作: 选出任一 x,满足 0 < x < ...

  4. 树形DP和状压DP和背包DP

    树形DP和状压DP和背包DP 树形\(DP\)和状压\(DP\)虽然在\(NOIp\)中考的不多,但是仍然是一个比较常用的算法,因此学好这两个\(DP\)也是很重要的.而背包\(DP\)虽然以前考的次 ...

  5. 《Data Structures and Algorithm Analysis in C》学习与刷题笔记

    <Data Structures and Algorithm Analysis in C>学习与刷题笔记 为什么要学习DSAAC? 某个月黑风高的夜晚,下班的我走在黯淡无光.冷清无人的冲之 ...

  6. PTA刷题笔记

    PTA刷题记录 仓库地址: https://github.com/Haorical/Code/tree/master/PTA/GPLT 两周之内刷完GPLT L2和L3的题,持续更新,包括AK代码,坑 ...

  7. dp乱写1:状态压缩dp(状压dp)炮兵阵地

    https://www.luogu.org/problem/show?pid=2704 题意: 炮兵在地图上的摆放位子只能在平地('P') 炮兵可以攻击上下左右各两格的格子: 而高原('H')上炮兵能 ...

  8. poj2411 Mondriaan's Dream (轮廓线dp、状压dp)

    Mondriaan's Dream Time Limit: 3000MS   Memory Limit: 65536K Total Submissions: 17203   Accepted: 991 ...

  9. Python 刷题笔记

    Python 刷题笔记 本文记录了我在使用python刷题的时候遇到的知识点. 目录 Python 刷题笔记 选择.填空题 基本输入输出 sys.stdin 与input 运行脚本时传入参数 Pyth ...

随机推荐

  1. 1. Spark Word Count

    1. request: 2. scala: sc.textFile("input").flatMap(_.split(" ")).map((_,1)).redu ...

  2. python时间Time模块

    时间和日期模块 关注公众号"轻松学编程"了解更多. python程序能用很多方式处理日期和时间,转换日期格式是一种常见的功能. python提供了一个time和calendar模块 ...

  3. [Luogu P2891/POJ 3281/USACO07OPEN ]吃饭Dining

    传送门:https://www.luogu.org/problemnew/show/P2891 题面 \ Solution 网络流 先引用一句真理:网络流最重要的就是建模 今天这道题让我深有体会 首先 ...

  4. SpringCloud之Gateway

    一.为什么选择SpringCloud Gateway而不是Zuul? Gateway和Zuul的职责一样,都承担着请求分发,类似Nginx分发到后端服务器. 1.SpingCloud Gateway ...

  5. 第4章 Function语意学

    第4章 Function语意学 目录 第4章 Function语意学 4.1 Member的各种调用方式 Nonstatic Member Function(非静态成员函数) virtual Memb ...

  6. Java每日一考202011.4

    1.JDK,JRE,JVM三者之间的关系 JDK包含JRE,JRE包含JVM JDK=JRE+JAVA的开发工具 JRE=JVM+JAVA核心类库 2.为什么要配置环境变量? 希望在任何路径下都能执行 ...

  7. python00

    # Python* [什么是 Python 生成器?](#什么是-Python-生成器)* [什么是 Python 迭代器?](#什么是-Python-迭代器)* [list 和 tuple 有什么区 ...

  8. Jquery禁用DIV鼠标右键

    $("#mp4").bind("contextmenu", function (e) { return false; });

  9. reids 入门

    1.reids 服务的安装有两种 1.1 exe文件安装,安装完成后,就直接在 "服务"列表中可以查看,并可以停止或启动 1.2 命令行安装:将文件解压至指定文件夹,CMD命令进入 ...

  10. 1+X云计算 应用商城系统(gpmall)-遇到的问题以及解决办法

    1+X云计算 应用商城系统(gpmall)-遇到的问题以及解决办法 问题1: 关于网站访问(打不开或者连接不上服务器的问题): 没有关闭selinux和防火墙,是访问不了网站 [root@mall ~ ...