计数dp,数位dp,状压dp,树形dp,记忆化搜索

dp个人感觉其实就是找一个递推式的过程,也就是状态转移方程。
然后再利用前面已经求出来的状态,得到某个状态的解

难点也就在于设定状态和求出转移方程

计数dp

顾名思义就是状态对应的值是一个东西的数量

acwing 900 数的划分
题目大意:
给定一个整数N(1<N<=1000),划分成若干个正整数的和,求方案数
并且:
假设N=4
1 1 2
2 1 1
1 2 1
是算一种方案

完全背包方法求解:
可以把这个题转化成完全背包
给定的数N,就是背包的容量,然后有N个物品价值和重量分别是
1 1
2 2
3 3
4 4

N N
然后求把背包放满的方案数

f[i,j]表示用前1到i这几个数把容量为j的背包填满的方案数

f[i,j]可以分成
没有i这个数的,f[i-1,j]
有1个i的,f[i-1,j-i]
有2个i的,f[i-1,j-2i]

有j/i个i的,f[i-1,j-j/i]
于是转移方程就是
f[i,j]=f[i-1,j]+f[i-1,j-i]+f[i-1,j-2i]…

然后就是对这个式子进行化简
考虑到式子中有j-i
所以可以看
f[i,j-i]=f[i-1,j-i]+f[i-1,j-2i]+f[i-1,j-3i]…
可以发现他和f[i,j]中后面一部分对应
所以得到
f[i,j]=f[i-1,j]+f[i,j-i]

然后化成一维的
f[j]=f[j]+f[j-i]

时间复杂度O(n^2)

#include<iostream>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N];
int n;
int main()
{
    cin>>n;
    f[0]=1;
    for(int i=1;i<=n;i++)
        for(int j=i;j<=n;j++)
            f[j]=(f[j]+f[j-i])%mod;
    cout<<f[n]<<endl;
    return 0;
}

另一种解法
一个数可以最少划分成1个,最多划分成N个
可以求出所有情况的方案数然后进行相加

设f[i,j]表示把数i划分成j份的方案数

f[i,j]可以分成
1.有一个1的,f[i-1,j-1]
2.没有1的,没有的话,就像当与i-j这个数划分成j份然后每份加一个1
f[i-j,j]

所以
f[i,j]=f[i-j,j]+f[i-1,j-1]

最后对f[n,1到n]求和就可以了

时间复杂度O(n^2)

#include<iostream>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N][N];
int main()
{
    int n;
    cin>>n;
    f[0][0]=1;
    for(int i=1;i<=n;i++)
    for(int j=1;j<=i;j++)
    f[i][j]=(f[i-1][j-1]+f[i-j][j])%mod;
    int res=0;
    for(int i=1;i<=n;i++) res=(res+f[n][i])%mod;
    cout<<res<<endl;
    return 0;
}

数位dp
顾名思义,也就是dp的对象是数的每一位
一般是需要注重分情况进行分析

acwing 338 计数问题
给定一个区间,l到r,问这个区间内0 到 9 这几个数在这个区间出现的次数

这里假如11,那么记录1出现了2次

0<l,r<100000000

这题数据范围很大,所以如果暴力求解的话,时间复杂度是O(1e8*数的最大位数)是会超时的,所以不能行

像这种求某一个区间的问题,可以先想一下前缀和思想看可不可行

在这里的话,假设现在在求l~r这个区间X出现的次数

那么就可以求出1~i-1中X出现的次数,和1到r中X出现的次数

l~r中X出现的次数==1到r中X出现的次数-1到i-1中X出现的次数

所以现在问题是怎样求1到r某一个区间中X出现的次数

假设现在r为
aaabccc
现在求b这个位置上X出现的次数

假设出现X的数为
xxxXyyy

1,当xxx<aaa的时候
xxx可以从000到aaa-1,有aaa种选法
yyy可以从000到999,有1000种选法
所以有aaa*1000种

2.,当xxx=aaa的时候

第一种情况:X<b
yyy就可以从000~999,1000种
第二种情况:X==b
yyy就可以从000~ccc,ccc+1种
第三种情况:x>b
0种

所以一个数在某一位出现的次数就是他们的和

特殊情况,但X为0的时候,aaa值能从001开始

所以就可以先枚举每一个数,求出1到r-1,和1到l-1得值.

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
typedef long long ll;
const int N=10;
int l,r;
ll get_aaa(int x,vector<int> bit)
{
    ll res=0;
    for(int i=bit.size()-1;i>x;i--) res=res*10+bit[i];
    return res;
}
ll get_ccc(int x,vector<int> bit)
{
    ll res=0;
    for(int i=x-1;i>=0;i--) res=res*10+bit[i];
    return res;
}
ll count(int r,int x)
{
   vector<int> bit;
   while(r)
   {
       bit.push_back(r%10);
       r/=10;
   }
   ll res=0;
   for(int i=0;i<bit.size();i++)
   {
       res+=(get_aaa(i,bit)-!x)*pow(10,i);
       if(x==bit[i]) res+=get_ccc(i,bit)+1;
       else if(x<bit[i]) res+=pow(10,i);
   }
   return res;
}
int main()
{
    while(cin>>l>>r&&l&&r)
    {
        if(l>r) swap(l,r);
        for(int i=0;i<=9;i++) cout<<count(r,i)-count(l-1,i)<<" "; 
        cout<<endl;
    }
    return 0;
}

状压dp

也顾名思义,就是把每一个状态压缩成一个二进制来表示。

不同的数代表不同的状态

acwing 291 蒙德里安的梦想

题目大意:
求把 N×M 的棋盘分割成若干个 1×2 的的长方形,有多少种方案。
1≤N,M≤11

也就相当于,现在有一个NM的矩阵,然后我们要添加1x2的格子进去,问可以填满的方案数

首先发现,当把格子横着放进去后,那么竖着放的格子就一种情况了,就是填进去

所以要求的就变成了,把1x2的格子横着放进去的合法方案数

然后又发现,当i-1列的格子放好了后,那么i列的格子方案数就只有1种

所以第i列的方案数可以由第i-1列求出来

然后第i-1对第i列的影响,就在于第i-1列,有几个1x2的格子横着放的,因为横着放,会占用第i列的格子

然后第i-1列放的情况数有2^n-1个,在这里就用状态压缩,用一个二进制来表示,加入0001000,就意味着第i列的第4行已经被占了,也就是i-1列的第4行是横着放的

设置状态
f[i,j] 表示第i列为被占的情况是 j 这个状态的所有方案数

状态计算

1,j已经确定,所有第i行中被占了的位置已经确定,不过它要合法的话,必须满足,1到1之间的0的个数必须是偶数,因为这样才可以放下一个竖着放的,并且放满

2.现在就考虑,第i列的方案数怎样由第i-1列求出

既然j已经确定,那么第i-1放的方案,就必须和j凑在一起合法,才算合法方案

第一,i-1放的方案,和i放的方案不能冲突,不能同时在某一行上横着放,因为第i-1列横着放了,第i列这个位置就没有了,假设第i-1列放的方案是k,那么k&j要为0

第二,他们一起,会改变第i行中,方格被占的情况,所有他们他们合在一起后,第i列被占的格子,也要满足1条件,也就是k|j,这个方案要满足第一条

综上所述

假设现在第i-1行的方案是k
那么当
1,k&j==0
2.k | j满足1条件

然后
f[i,j]+=f[i-1,k]

最后我们的答案就是 f[m,0],也就是第m列,它这列格子都没有被占的情况

时间复杂度
枚举一个状态
2^11
设F=(2^11)
枚举j的状态和k的状态是否合法
FF
然后是要求1到m列
于是时间复杂度就是
11
FF约等于20482048*11=46137344,是可以过的

#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
const int N=20,M=1<<11;
long long f[N][M];
bool book[M];//用来标记一列别占的状态中其中那些合法
vector<vector<int>> is_ok(M);//用来标记is_ok[i]中保存着第合法的第i列它和第i-1列那些方案凑在一起是合法的
int n,m;
int main()
{
    while(cin>>n>>m,n||m)
    {
        for(int i=0;i<1<<n;i++)//寻找一列中,那些方案是合法的
        {
            int cnt=0;
            bool is_true=true;
            for(int j=0;j<n;j++)
            if(i>>j&1)
            {
                if(cnt&1)
                {
                    is_true=false;//表示1到1之间的0的个数是奇数
                    break;
                }
                cnt=0;
            }
            else cnt++;
            if(cnt&1) is_true=false;
            book[i]=is_true;
        }
        for(int i=0;i<1<<n;i++)//寻找第i列的方案可以和i-1的那些方案合法
        {
            is_ok[i].clear();//情况上一组数据的数据
            for(int j=0;j<1<<n;j++)
            if((i&j)==0&&book[i|j]) is_ok[i].push_back(j);
        }
        memset(f,0,sizeof f);
        f[0][0]=1;//状态中可以确定的,就是0列,它的格子是肯定不会被前面占用的,所以方案数为1
        for(int i=1;i<=m;i++)
        for(int j=0;j<1<<n;j++)
        for(auto k:is_ok[j])
        f[i][j]+=f[i-1][k];
        cout<<f[m][0]<<endl;
    }
    return 0;
}

acwing 91 最短Hamilton路径

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。
1≤n≤20
0≤a[i,j]≤107

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

题中要求最短的话

然后又要求每一个点都有要经过,那就代表是有几条,至少一条路径是可以遍历所有点的

找到其中最短的一条路径

然后每条路径的不同,就代表走的边不同,走的边不同 ,就代表点的访问顺序不同

那么就代表从0点走到i点,最小,就是到它之前经过的所有点,再到它的所有可行的方案中最小

然后经过的点的情况,进行状态压缩
0001010表示走过第2个点,和第4个点的情况

设置状态
f[i,j]表示 经过i这个状态的时候到达j的最小

状态计算
首先要保证i的情况中必须由j
然后枚举所有经过的点中,从其中某一个点到j的路径值,取最小的

时间复杂度
枚举所有经过点的情况2^20
枚举每个点20
判断每个点是否经过了20
20x20x2^20==419430400,是可以过的

所以最后答案就在
f[(1<<n)-1,n-1]

#include<iostream>
#include<cstring>
using namespace std;
const int N=25,M=1<<20;
int f[M][N];
int a[N][N];
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    for(int j=0;j<n;j++)
    cin>>a[i][j];
    memset(f,0x3f,sizeof f);
    f[1][0]=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>>k&1) f[i][j]=min(f[i][j],f[i-(1<<j)][k]+a[k][j]);//i-(1<<j)表示是每经过j这个点的情况
        }
    }
    cout<<f[(1<<n)-1][n-1]<<endl;
    return 0;
}

状压的话,感觉就是要枚举情况的时候,不好枚举,就把它压缩成一个二进制进行枚举

树形dp

顾名思义也就是,dp的时候,是在树上面进行dp的

acwing 285 没有上司的舞会

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

1≤N≤6000,
−128≤Hi≤127

题中指明了,每个人的关系就像一课树

然后要求,取了父节点的话,就不会取它的子节点

所以父节点有取和不取2种情况

1.当父节点不取的时候,那么它的子节点都可以取

2.父节点取的时候,那么它的子节点都不能取

父节点取和不取可以决定最后的值

所以从根节点开始

讨论它取和不取的情况,可以一层一层的由它的子节点得到对应的最大的价值

设置状态
f[i,j],j的取值由0,1
0表示不取这个点
1表示取这个点

状态计算
f[i,0]==所有子节点max(f[子节点,0],f[子节点,1])的和

f[i,1]==所有子节点f[i,0]的和+happy[i]

因为计算需要用的一个根节点的子节点,所以需要先在树上进行递归,把子节点的先都算出来

#include<iostream>
#include<cstring>
using namespace std;
const int N=6010;
int ne[N],h[N],val[N],indx,happy[N],du[N];
int n,f[N][2];
void add(int a,int b)
{
    val[indx]=b,ne[indx]=h[a],h[a]=indx++;
}
void dfs(int x)
{
    f[x][1]+=happy[x];
    for(int i=h[x];i!=-1;i=ne[i])
    {
        int j=val[i];
        dfs(j);
        f[x][0]+=max(f[j][0],f[j][1]);
        f[x][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=1;i<=n-1;i++)
    {
        int a,b;
        cin>>a>>b;
        add(b,a);
        du[a]++;
    }
    int boot;
    for(int i=1;i<=n;i++)
    if(du[i]==0)
    {
        boot=i;
        break;
    }
    dfs(boot);
    cout<<max(f[boot][0],f[boot][1])<<endl;
    return 0;
}

记忆化搜索

搜索+dp

搜索的话,可能会处理很多次一样的问题

在这里,就把已经计算出来的,记录下来,当下次要用的时候,就直接用,不用再进行计算

acwing 滑雪
给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。

矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。

一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。

当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。

现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。

1≤R,C≤300,
0≤矩阵中整数≤10000

诈一看,这就是一个搜索题,找到能走的最长的那条路径

but ,他的起点是不确定的,所以要每个起点都找一遍,在这个过程中是有很多重复计算的,所以记录下来就好了

设置f[i,j]表示从矩阵中i,j这个点出发的,可以走的最长路径

最后找到f中的最大值就可以了

#include<iostream>
#include<cstring>
using namespace std;
const int N=310;
typedef long long ll;
ll a[N][N],f[N][N];
int n,m;
int ne[4][2]={1,0,-1,0,0,1,0,-1};
ll dfs(int x,int y)
{
    if(f[x][y]!=-1) return f[x][y];
    if(f[x][y]==-1) f[x][y]=1;
    for(int i=0;i<4;i++)
    {
        int t1=x+ne[i][0],t2=y+ne[i][1];
        if(t1>=1&&t1<=n&&t2>=1&&t2<=m&&a[t1][t2]<a[x][y])
        f[x][y]=max(f[x][y],dfs(t1,t2)+1);
    }
    return f[x][y];
}
int main()
{
    memset(f,-1,sizeof f);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
    cin>>a[i][j];
    ll res=0;
    for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
    {
        res=max(res,dfs(i,j));
    }
    cout<<res<<endl;
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值