[总结][笔记][题解]DP专题
\(DP\)专题
C'est dur.
数位\(DP\)
原题链
算法概述
数位\(DP\)主要求解的是在给定区间\(l,r\)中满足条件的解的个数这一类问题.我们一般把求区间\(l,r\)转换成求区间\(0,r\)和\(0,l-1\),再将得到的结果相减就是答案.
对于\(不要62\)这个问题,我们可以设\(f[i][j]\)表示位数为\(i\),开头的数为\(j\)的情况下满足条件的答案数,但要注意的是一般情况下,区间\(l,r\)是限制也就是说不能包括,但这道题是要包括两个端点的,也就是说如果我们计算\(0,r\)这个区间实际上是计算\([0,r)\),但是要求的却是\([0,r]\)所以我们在一开始的时候就给右端点的位数\(+1\)即可.
那么递推式也就很好表示了,对于\(f[i][j]\),如果\(j=4\)那么\(f[i][j]=0\),如果\(j!=4\),\(f[i][j]=\sum_{k=0}^{9}(f[i-1][k])\)并且要保证(k!=2||j!= 6)&&k!=4
,于是我们枚举\(i,j,k\)即可完成对\(f\)数组的初始化
对于求解的部分,如果要求解\(355\),那么我们就需要把这个数一位一位的拆出来才能限制,也就是说我们根据拆出来的数就可以求\(f[1][0]~f[1][9]\),\(f[2][0]~f[2][9]\),\(f[3][0]~f[3][5]\),从而得到答案,如果在求解的过程中遇到拆出来的一个数是\(4\)就可以直接退出了,因为这意味着以后的枚举的没一个数都会含有\(4\)而这是不符合要求的,对于遇到\(62\)也同理
上代码
#include <bits/stdc++.h>
using namespace std;
int n,m;
int f[10][10];
void init(){
memset(f,0,sizeof(f));
f[0][0] = 1;
for(int i = 1;i <= 7;i++){
for(int j = 0;j <= 9;j++){
for(int k = 0;k <= 9;k++){
if(k != 4 && j != 4 && !(j == 6 && k == 2))
f[i][j] += f[i - 1][k];
}
}
}
}
int get(int k){
if(k == 0)return 0;
int len = 0,d[10],rhs = 0;
while(k){//拆数
d[++len] = k % 10;
k /= 10;
}
d[len + 1] = 0;
for(int i = len;i > 0;i--){
for(int j = 0;j < d[i];j++){
if(j != 4 && !(j == 2 && d[i + 1] == 6))
rhs += f[i][j];
}
if(d[i] == 4 || (d[i] == 2 & d[i + 1] == 6))
break;
}
return rhs;
}
int main(){
while(scanf("%d%d",&n,&m)){
if(n == 0 && m == 0)return 0;
init();
printf("%d\n",get(m + 1) - get(n));
}
return 0;
}
单调队列优化DP
可以参考我写的琪露诺的博客
斜率优化DP
原题链
斜率优化讲解
对于洛谷上的这道题,我们设\(sum[k] = \sum_{i=1}^kC[i]+1\)(可以把S数组视为\(C\)的前缀和),用\(dp[i]\)表示装好前\(i\)个玩具的最小费用,不是长度,那么我们可以得到转移方程:
此时暴力进行\(DP\)是\(O(n^2)\)的,仍然要优化.
对于每一个\(dp[i]\)都是由一个\(j\)推到而来的,并且这个\(j\)是对于当前的\(i\)的最优决策.现在假设有\(j_1,j_2(1{\leq}j_1{<}j_2{<}i)\),并且决策\(j_2\)优于\(j_1\),那么就有
那么也就是说如果有\(j_1,j_2\)满足这两个式子,那么\(j_2\)一定比\(j_1\)要更优.
那么为什么要叫斜率优化呢?其实我们观察最终得到的式子可以吧\(dp[i]+g[i]\)看做纵坐标,\(f[i]\)看做横坐标,那么这个式子其实就相当于\(k=\frac{{\Delta}y}{{\Delta}x}\),也就是一个一次函数的斜率表达式,当这个斜率\(k \leq 2f[i]\)时则\(j_2\)优于\(j_1\).
根据上面推出来的关系我们就可以用最优的决策来更新答案了,代码就是上面公式的翻译,加上一个单调队列
代码
#include <bits/stdc++.h>
using namespace std;
long long n,l;
long long sum[500010],f[500010],head,tail,q[50010],dp[500010],g[500010];
double slope(int x,int y){
return (double)(dp[y] + g[y] - dp[x] - g[x]) / (f[y] - f[x]);
}
int main(){
scanf("%lld%lld",&n,&l);
for(int i = 1;i <= n;i++){
scanf("%lld",&sum[i]);
sum[i] += sum[i - 1];
f[i] = sum[i] + i;
g[i] = (f[i] + l + 1) * (f[i] + l + 1);
}
g[0] = (l + 1) * (l + 1);
for(int i = 1;i <= n;i++){
while(head < tail && slope(q[head],q[head + 1]) <= 2 * f[i])head++;
dp[i] = dp[q[head]] + (f[i] - f[q[head]] - l - 1) * (f[i] - f[q[head]] - l - 1);
while(head < tail && slope(q[tail],i) < slope(q[tail - 1],q[tail]))tail--;
q[++tail] = i;
}
printf("%lld\n",dp[n]);
return 0;
}
状压\(DP\)&洛谷P1433
原题链
状压\(DP\)
状压\(dp\)是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的 -- \(OI wiki\)
状态压缩的思想是用二进制来表示状态.
状压\(dp\)的时间复杂度是\(O(n^22^n)\)的,通常只能用于\(n \leq 21\)的数据范围
题目思路
我们定义数组\(dp[i][j]\)表示的是老鼠走到第\(i\)个奶酪,且之前走过的状态为\(j\)时所用的最短的距离.举个栗子:\(dp[9][166]\)代表的就是老鼠走到了第\(9\)块奶酪,之前已经走过第\(2,3,6,8\)块奶酪,因为\(166\)用二进制表示就是\(10100110\),值得注意的是,这里的第\(i\)状态是从二进制低位到高位来算的.
对于\(dp\)数组的初始化:首先先赋值为最大值,可以使用memset(dp,127,sizeof(dp))
,接着如果只经过第\(i\)块奶酪,那么对应的\(dp\)数组的值就是原点到这个奶酪的坐标的直线距离.用程序表示就是dp[i][1 << (i - 1)] = dis[0][i]
.为什么是左移\(i-1\)位呢?我们可以手推一下,假设现在我们只到了第\(1\)块奶酪,那么对应的状态(二进制)因该是\(0001\),转换为十进制就是\(1\),\(1=2^0\),所以我们其实是左移\(0\)位,如果到了第\(2\)块奶酪,则二进制下的表示是\(00010\),对应的十进制就是\(2\),\(2 = 2^1\)所以实际上左移了\(1\)位,这也就是左移\(i-1\)位的原因.
接下来就是转移方程了,\(dp[i][k] = min(dp[i][k],dp[j][k-2^{i-1}] + dis[i][j])\),\(dp[j][k-2^{i-1}]\)表示的是现在老鼠在\(j\)点,并且没有走过\(i\)点的最短距离,\(dis[i][j]\)就是\(i,j\)之间的距离,为什么可以表示没有走过\(i\)的状态呢?因为\(dp[i][k]\)要求的就是走过了\(i\)的最佳答案,所以\(k\)的二进制表示中在\(i\)那一位是\(1\),而\(dp[j][k-2^{i-1}]\)中,\(k\)减去了\(2^{i-1}\)所以刚好把\(k\)二进制表示中\(i\)那一位的\(1\)减成了\(0\),至于为什么是\(2^{i-1}\)前文有解释.
关于统计答案,就是找出\(min(dp[i][2^n-1])\)就可以了,因为已经明确了最终的状态是全部奶酪都要吃,所以对于每一个\(i\)只要保证描述状态的那一维也就是\(2^n-1\)也就保证了每一个奶酪都被吃了,所以每一个\(i\)都是成立的合法解,所以取最小值就行.再理解一下,为什么\(2^n-1\)就可以保证代表每一个奶酪都被取到了呢?根据上文的描述我们知道奶酪\(i\)被取了就意味着在\(dp\)数组第二维描述状态的数的二进制位上第\(i\)位是\(1\),那么其实我们只要在统计答案的时候保证描述状态的每一位都是\(1\)即可,那么我们先假设\(n=3\),那么对应的最终状态的二进制表示因该是\(111\),十进制下就是\(7\),而\(7=8-1,8=2^3,8\)的二进制表示是\(1000\),那么再在\(8\)的二进制位上\(-1\),就变成\(111\),也就是答案了.
这就是状压\(dp\)的基本思路和解法,其实应用过的范围不是很广,因为可以支持的数据范围不大,但是这种用二进制来优化状态表示和转移的方法还是很有学习价值的.
解题代码
下面就是洛谷上例题的代码了:
在实现的时候还是要注意精度,因为要不然的话太小的小数比如\(0.001\)就会被计算成\(0\),导致\(WA\),所以尽量所有含有实数运算的函数和参数都要定义成\(double\)类型
#include <bits/stdc++.h>
using namespace std;
double dis[20][20];
struct point{
double x,y;
}pos[20];
double dp[20][34000];
int n;
double pow(double x){
return x * x;
}
double calc_dis(int x,int y){
return sqrt(pow(pos[x].x - pos[y].x) + pow(pos[x].y - pos[y].y));
}
int main(){
double ans;
memset(dp,127,sizeof(dp));
ans = dp[0][0];
scanf("%d",&n);
for(int i = 1;i <= n;i++){
scanf("%lf%lf",&pos[i].x,&pos[i].y);
}
pos[0].x = pos[0].y = 0;
for(int i = 0;i <= n;i++){
for(int j = i + 1;j <= n;j++){
dis[i][j] = dis[j][i] = calc_dis(i,j);
}
}
for(int i = 1;i <= n;i++)
dp[i][1 << (i - 1)] = dis[0][i];
for(int k = 1;k < (1 << n);k++){
for(int i = 1;i <= n;i++){
if((k & (1 << (i - 1))) == 0)continue;
for(int j = 1;j <= n;j++){
if(i == j)continue;
if((k & (1 << (j - 1))) == 0)continue;
dp[i][k] = min(dp[i][k],dp[j][k - (1 << (i - 1))] + dis[i][j]);
}
}
}
for(int i = 1;i <= n;i++)ans = min(ans,dp[i][(1 << n) - 1]);
printf("%.2lf\n",ans);
return 0;
}