「动态规划」例题之数学相关的状态和转移方程的设计

0x50「动态规划」例题

这类问题可能常用到的相关知识:容斥原理,组合数递推,乘法逆元(用于计算组合数),快速幂。

计数类DP

为了保证DP计数过程中的不重不漏,这一类DP问题往往会有些特殊的“子结构”或者说是“阶段”,我们的原则是——找到一个“基准点”,围绕它构造一个“不可分割”的整体,避免子问题重叠。

例题

POJ1737 Connected Graph
状态表示:F[i]表示i个节点的无向连通图有多少个
设计转移方程时,我们考虑将1号点作为基准点。首先用容斥原理考虑,n个点的无向图连通图个数=n个点的无向图个数-n个点的无向不连通图。其中,任意两个点之间可以建边,总共最多可建C_{n}^{2}条边,每条边可以有两种选择:连接或断开。因此n个点的无向图个数是2^{C_{n}^{2}}。考虑计算n个点的无向不连通图的方案数,由于一个无向不连通图必然由多个连通块构成,于是设1号点所在的连通块的大小为j,那么从2...n这n-1个点中选出j-1个点与1号点构成连通块的方案数是C_{n-1}^{j-1},剩下的点随意组合,方法数为2^{C_{n-j}^{2}}
转移方程:F[i]=2^{C_{i}^{2}}-\sum_{j=1}^{i-1}F[j]*C_{i-1}^{j-1}*2^{C_{i-j}^{2}},其中 0\leq j<iA[j]<A[i]
边界:F[1]=1
目标:F[n]
由于n较小,可直接用帕斯卡公式递推组合数,双重循环DP,时间复杂度:O(n^{2})
PS:本题因为答案很大,所以需要高精度,用这个公式实现高精度,需要维护高精度加法,高精度减法,高精度乘法,高精度低精度乘法,高精度快速幂五种操作。于是,代码中method_1是不含高精度的,method_2是含有高精度,但是使用的直接递推(非容斥原理),代码如下

/*

*/
#define method_2
#ifdef method_1
/*
n>9时int溢出 
https://blog.csdn.net/icefox_zhx/article/details/79055962

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=50+5;
const int INF=0x3f3f3f3f;
int n,c[maxn][maxn],f[maxn];
void pre1(){
    c[0][0]=1;
    for(int i=1;i<=50;i++){
        for(int j=0;j<=i;j++){
            c[i][j]=c[i-1][j-1]+c[i-1][j];
        }
    }
}
void pre2(){
    f[1]=1,f[2]=1,f[3]=4;
    for(int i=3;i<=50;i++){
        f[i]=(1<<c[i][2]);
        for(int j=1;j<=i-1;j++){
            f[i]-=f[j]*(1<<c[i-j][2])*c[i-1][j-1];
        }
    }
}
int main() {
    ios::sync_with_stdio(false);
    freopen("Connected Graph.in","r",stdin);
    pre1();
    pre2();
    while(cin>>n&&n){
        cout<<f[n]<<endl;
    }
    return 0;
}
#endif
#ifdef method_2
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=50+5;
const int maxl=400+5;
const int INF=0x3f3f3f3f;
ll n,c[maxn][maxn],bin[maxn];
struct bigint{
    ll a[maxl],n;
    bigint(){memset(a,0,sizeof(a)),n=0;}
    ll& operator[](int x){return a[x];}
    void print(){for(int i=n;i>=1;i--) cout<<a[i];}
    friend bigint operator+(bigint x,bigint y){
        bigint res;res.n=max(x.n,y.n);
        for(int i=1;i<=res.n;i++) res[i]=x[i]+y[i];
        for(int i=1;i<=res.n;i++) res[i+1]+=res[i]/10,res[i]%=10;
        if(res[res.n+1]) res.n++;
        return res;
    }
    friend bigint operator*(bigint x,bigint y){
        bigint res;res.n=x.n+y.n-1;
        for(int i=1;i<=x.n;i++) for(int j=1;j<=y.n;j++)res[i+j-1]+=x[i]*y[j];
        for(int i=1;i<=res.n;i++) res[i+1]+=res[i]/10,res[i]%=10;
        while(res[res.n+1]) res.n++,res[res.n+1]+=res[res.n]/10,res[res.n]%=10;
        return res;
    }
    friend bigint operator*(bigint x,ll y){
        bigint res;res.n=x.n;
        for(int i=1;i<=x.n;i++) res[i]+=x[i]*y;
        for(int i=1;i<=res.n;i++) res[i+1]+=res[i]/10,res[i]%=10;
        while(res[res.n+1]) res.n++,res[res.n+1]+=res[res.n]/10,res[res.n]%=10;
        return res;
    }
}f[maxn];
void pre1(){
    c[0][0]=1;
    bin[0]=1;
    for(int i=1;i<=50;i++){
        bin[i]=bin[i-1]<<1;
        for(int j=0;j<=i;j++){
            c[i][j]=c[i-1][j-1]+c[i-1][j];
        }
    }
}
void pre2(){
    f[1][1]=1,f[1].n=1,f[2][1]=1,f[2].n=1;
    for(int i=3;i<=50;i++){
        for(int j=1;j<=i-1;j++){
            f[i]=f[i]+f[i-j]*f[j]*c[i-2][j-1]*(bin[j]-1);
        }
    }
}
int main() {
    ios::sync_with_stdio(false);
    //freopen("Connected Graph.in","r",stdin);
    pre1();
    pre2();
    while(cin>>n&&n){
        f[n].print();E;
    }
    return 0;
}
#endif
#ifdef method_3
/*

*/

#endif

POJ1037 A Decorative Fence
由于要求排名C的栅栏的形状,所以用类似数位DP中的常用手段——试填法。具体地说,我们从小到大枚举第一块木板的高度,若后面n-1块木板的排名小C则继续循环,同时令C减去第一块高度为len的方案数。若后面n-1块木板的排名大于等于C则第一块木板的高度就应该是C,这是试填成功,break出当前循环,继续试填下一块木板。为了高效计算,我们预处理出当第一块木板高度确定时,剩下所有木板构成的方案数。
状态表示:F[i,j,k]表示i块长度不同的木板,最左边木板长度从小到大排在第j位,位置状况为k(k=0表示处于低位,k=1表示处于高位)的方案数。
设计转移方程时,由于高低位相间,所以阶段i和阶段i-1之间构成重叠子问题,使得DP原理合法。
转移方程:
F[i,j,0]=\sum_{p=j}^{i-1}F[i-1,p,1]
F[i,j,1]=\sum_{p=0}^{j-1}F[i-1,p,0]
PS:这个转移方程里有一点格外重要,即p的范围问题。首先,若最左边的木板高度上排在第j位,同时处于低位。那么它的前一块木板必然处于高位,并且其高度排名大于等于j,注意这里是大于等于的原因在于去掉最左边的木板时,所有高度排名大于它的木板的高度排名都会-1,而所有高度小于它的木板的高度排名不变,所以才出现了p可以取到j的情况。而若最左边的木板若是处于高位,那么去掉它之后,下一个处于低位的木板的排名必然严格小于j。
PPS:第一块木板可能既处于高位,也可能处于低位,所以单独处理。
边界:F[1,1,0]=F[1,1,1]=1
目标:准备拼凑答案状态,方法已在上面讲过,这里不再赘述
三重循环DP,时间复杂度:O(n^{3}),其中试填法复杂度:O(n^{2})
代码如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=20+5;
const int INF=0x3f3f3f3f;
int T,n,used[maxn];
ll m,f[maxn][maxn][2];
void pre(){
    f[1][1][0]=f[1][1][1]=1;
    for(int i=2;i<=20;i++){
        for(int j=1;j<=i;j++){
            for(int k=1;k<=j-1;k++){
                f[i][j][1]+=f[i-1][k][0];
            }
            for(int k=j;k<=i-1;k++){
                f[i][j][0]+=f[i-1][k][1];
            }
        }
    }
}
int main() {
    ios::sync_with_stdio(false);
//  freopen("A Decorative Fence.in","r",stdin);
    pre();
    cin>>T;
    while(T--){
        cin>>n>>m;
        memset(used,0,sizeof(used));
        int last,k;
        for(int j=1;j<=n;j++){
            if(f[n][j][1]>=m){
                last=j,k=1;
                break;
            }
            else m-=f[n][j][1];
            if(f[n][j][0]>=m){
                last=j,k=0;
                break;
            }
            else m-=f[n][j][0];
        }
        cout<<last<<" ";
        used[last]=1;
        for(int i=2;i<=n;i++){
            k^=1;
            int j=0;
            for(int len=1;len<=n;len++){
                if(used[len]) continue;
                j++;
                if((k==0&&len<last)||(k==1&&len>last)){
                    if(f[n-i+1][j][k]>=m){
                        last=len;
                        break;
                    }
                    else m-=f[n-i+1][j][k];
                }
            }
            cout<<last<<" ";
            used[last]=1;
        }
        cout<<endl;
    }
    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif

数位统计DP

数位DP往往是通过预处理DP,然后通过类似倍增DP中的拼凑思想——“试填法”求解。(例题:POJ 3208、BZOJ 1799)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容