第十六课:数位dp、状压dp、树形dp、记忆化搜索

 

目录

一、数位dp(分情况讨论)

 二、状态压缩dp

例题1、蒙德里安的梦想

例题2、最短哈密顿距离

三、树形dp

四、记忆化搜索


一、数位dp(分情况讨论)

统计数的出现次数:

分析:直接统计数字x在1~n之间出现的次数,然后利用前缀和的性质,即可求得数字x在a~b之间出现的次数。

分情况讨论:以数字abcdefg为例,统计数字x,在第四位(d)上在1~n出现的次数。其情况有如下几种:

  • 后三位小于abc,则后三位取值有000~abc-1,前三位数字可取000~999,则情况数为abc*1000
  • 后三位等于abc:
    • 若d>x:则后三位数字可取000~999
    • 若d==x:则后三位数字可取000~efg
    • 若d<x:情况不存在 

  • 若x==0,后三位等于abc的情况与上述一致。
  • 当后三位小于abc时,若后三位取000,则出现前导0,表示不合法,不会计算x的出现次数,因此后三位取值是001~abc,即少了1000种情况。

由上述知,对x在第i位,设这个数N有n位:

  • 若x在最高位,则只有后面位数等于abc的情况。x是0的话,则不需枚举
  • 若x不在最高位
    • i+1~n位<原数时,情况数为(i+1~n位-!x)*10^i
    • i+1~n位==原数时
      • 若第i位数大于x,情况数为(10^i)
      • 若第i位数等于x,情况数为(1~i位+1)

 把在每一位上出现的次数加起来即得1~N内x出现的次数,利用前缀和性质可算出答案。

代码如下:

//这里填你的代码^^
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
/*

001~abc-1, 999

abc
    1. num[i] < x, 0
    2. num[i] == x, 0~efg
    3. num[i] > x, 0~999

*/

int get(vector<int> num,int l,int r)
{
    int res=0;
    for(int i=l;i>=r;i--)
        res=res*10+num[i];
    return res;
}
int power10(int x)
{
    int res=1;
    while(x--) res*=10;
    return res;
}

int count(int n,int x)
{
    if(!n) return 0;

    vector<int> num;
    while(n)
    {
        num.push_back(n%10);
        n/=10;
    }
    n=num.size();

    int res=0;
    for(int i=n-1-!x;i>=0;i--)
    {
        if(i<n-1)
        {
            res+=get(num,n-1,i+1)*power10(i);
            if(!x)  res-=power10(i);
        }
        if(num[i]==x) res+= get(num,i-1,0)+1;
        else if(num[i]>x)  res+=power10(i);
    }
    return res;
}



int main()
{
    int a,b;
    while(cin>>a>>b,a)
    {
        if(a>b) swap(a,b);
        for(int i=0;i<=9;i++)
            cout<<count(b,i)-count(a-1,i)<<" ";
        cout<<endl;
    }
    return 0;
}


//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3924723/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 二、状态压缩dp

状态压缩dp通常是将每一种状态转化为二进制数,从而枚举每一个二进制数,从而判断每一种状态。

例题1、蒙德里安的梦想

 题目分析:

 如果横向的长方形摆放完了,那么纵向的长方形只能一个一个填入棋盘中。因此总方案数等于横向摆放长方形的方案数。

  • 状态表示f(i,j):表示前i-1列已经摆放完,第i-1列伸到第i列的状态是j的摆放第i列的方案数。属性为数量。
  • 状态计算:集合划分依据为摆放第i-1列时,第i-2列伸出到第i-1列的所有可能状态。

枚举第i列j的所有状态,加上第i-1列能转移到j的状态k的方案数:

       f[i,j]=\sum f[i-1,k]

状态k能转移到j的判断标准:

  1. 摆放不冲突,不摆放到已经被占用的位置,即j&k==0。
  2. 摆放后,连续的0的数量为偶数。
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
using namespace std;
const int N =12,M=1<<N;

typedef long long LL;

vector<int> state[M];//预处理可以转移到这个状态的所有状态
bool st[M]; //判断这个状态是否符合连续的0数量是偶数个
int n,m;
LL f[N][M];//前n-1列已经摆好,正在摆放第n列,且n-1列“伸出”到第n列的状态是j

int main()
{
    while(cin>>n>>m,n||m)
    {
        //预处理满足连续0数量偶数个的状态
        for(int i=0;i<1<<n;i++)
        {
            bool is_valid =true;
            int cnt=0;//记录连续0数量
            for(int j=0;j<n;j++)//n行 位数为n 所以循环n次
                if(i>>j &1)//如果该位是1
                {
                    if(cnt&1)//如果是奇数
                    {
                        is_valid=false;
                        break;
                    }
                    cnt=0;
                }
                else cnt++;
            if(cnt&1) is_valid=false;//最后剩一段需要额外判断
            st[i]=is_valid;
        }

        for(int i=0;i<1<<n;i++)
        {
            //存储能转移去 i 的状态
            state[i].clear();//把上次用过的清空
            for(int j=0;j<1<<n;j++)
                if((i&j)==0&&st[i|j])
                    state[i].push_back(j);
        }

        memset(f,0,sizeof f);
        f[0][0]=1;//一个都不放 方案数为1

        //实际上规定棋盘是0到m-1,从1开始遍历意思是第0列已经摆好。
        for(int i=1;i<=m;i++)
            for(int j=0;j<1<<n;j++)//枚举第m层的所有状态
                for(auto k:state[j])
                    f[i][j]+=f[i-1][k];

        cout<<f[m][0]<<endl;
    }
    return 0;
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3922502/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

例题2、最短哈密顿距离

题目分析:

  • 状态表示f(i,j):移动到第j个点时,走过的情况为i的所有方案,属性为min。
  • 状态计算: 集合划分为走到前一个点k时,所有可能的状态s。
  • 状态转移方程:f[i,j]=min(f[i,j],f[s,k])\ \ \ k= 1,2,,,n

步骤:先遍历所有状态,遍历所有点,找出经过该点i的状态。遍历所有前一个点,找出合法状态(未经过i点,但经过k点),然后利用状态转移方程。

//这里填你的代码^^
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =20,M=1<<N;
int w[N][N];
int f[M][N];
int n;
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            cin>>w[i][j];

    memset(f,0x3f,sizeof f);
    f[1][0]=0;  //只走过了0这个点

    for(int i=0;i<1<<n;i++)
        for(int j=0;j<n;j++)
            if(i>>j&1)
                for(int k=0;k<n;k++)
                    if((i-(1<<j))>>k&1)
                        f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);

    cout<<f[(1<<n) -1][n-1];
    return 0;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3923470/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

三、树形dp

 题目分析:

  • 状态表示f(i,j):表示j为0、1,表示选择或不选择第i个点。集合在以第i个点为根的子树中选择,选或不选第i个点的最大happy值。属性为max
  • 状态转移方程
    • 如果j为0,则最佳方案中,其子节点有可能选也有可能不选。对所有子节点kf[i,0]+=max(f[k,0],f[k,1]);
    • 如果j为1,则最佳方案中,其子节点肯定不选。对所有子节点kf[i,j]+=f[k,0]

实现方式:从根节点开始搜,搜索其所有子节点。然后递归搜索子节点的子节点,逐步返回f。

最后找出根节点root,答案即为max(f[root,1],f[root,0])

代码如下:

//这里填你的代码^^
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =6010;

int happy[N];
int h[N],e[N],ne[N],idx;
int f[N][2];
bool has_father[N]; //找根节点
int n;

void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

void dfs(int u)
{
    f[u][1]=happy[u];

    for(int i=h[u];i!=-1;i=ne[i])
    {
        int j=e[i];

        dfs(j);

        f[u][0]+=max(f[j][0],f[j][1]);
        f[u][1]+=f[j][0];

    }
}



int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>happy[i];

    memset(h,-1,sizeof h);

    for(int i=0;i<n-1;i++)
    {
        int a,b;
        cin>>a>>b;
        has_father[a]=true;
        add(b,a);
    }

    int root=1;
    while(has_father[root])  root++;

    dfs(root);

    cout<<max(f[root][0],f[root][1]);
    return 0;
}



//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3923838/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

四、记忆化搜索

用f[N,N]存储每个点的最长距离,这样在遍历到某个点时,直接加上其f值即可。

实现:搜索某个点时,如果搜索过就直接返回。如果未搜索过,首先f置为1,搜索四周合法点,f为其中最大值加上1。

代码如下 :

//这里填你的代码^^
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =310;

int n,m;
int f[N][N];
int h[N][N];

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

int dp(int x,int y)
{
    int& v=f[x][y];
    if(v!=-1)  return v;

    v=1;
    for (int i = 0; i < 4; i ++ )
    {
        int a = x + dx[i], b = y + dy[i];
        if (a >= 1 && a <= n && b >= 1 && b <= m && h[x][y] > h[a][b])//能走通
            v=max(v,dp(a,b)+1);

    }
    return v;
}



int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>h[i][j];

    memset(f,-1,sizeof f);

    int res=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
                res=max(res,dp(i,j));

    cout<<res;
    return 0;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/3924139/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值