动态规划-计数、数位统计、状态压缩、树形、记忆化搜索

计数类DP

整数划分

问题描述

一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。

我们将这样的一种表示称为正整数 n 的一种划分。

现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。

输入格式

共一行,包含一个整数 n。

输出格式

共一行,包含一个整数,表示总划分数量。

由于答案可能很大,输出结果请对 109+7 取模。

数据范围

1≤n≤1000

输入样例

5

输出样例

7

问题分析

解法1:把1,2,3, … n分别看做n个物体的体积,这n个物体均无使用次数限制,问恰好能装满总体积为n的背包的总方案数(完全背包问题变形)

初值问题:
求最大值时,当都不选时,价值显然是 0
而求方案数时,当都不选时,方案数是 1(即前 i 个物品都不选的情况也是一种方案),所以需要初始化为 1
即:for (int i = 0; i <= n; i ++) f[i][0] = 1;
等价变形后: f[0] = 1

状态计算:

f[i][j]表示前i个整数(1,2…,i)恰好拼成j的方案数
求方案数:把集合选0个i,1个i,2个i,…全部加起来
f[i][j] = f[i - 1][j] + f[i - 1][j - i] + f[i - 1][j - 2 * i] + …;
f[i][j - i] = f[i - 1][j - i] + f[i - 1][j - 2 * i] + …;
因此 f[i][j]=f[i−1][j]+f[i][j−i]; (这一步类似完全背包的推导)
解法2:
在这里插入图片描述

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1010,MOD=1e9+7;
int n;
int f[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;
}
#include<bits/stdc++.h>
using namespace std;
const int N=1010,MOD=1e9+7;
int n;
int f[N][N];
int main()
{
    cin>>n;
    f[1][1]=1;
    for(int i=2;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

计数问题

问题描述

给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9 的出现次数。

例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中 0 出现 10 次,1 出现 10 次,2 出现 7 次,3 出现 3 次等等…

输入格式

输入包含多组测试数据。

每组测试数据占一行,包含两个整数 a 和 b。

当读入一行为 0 0 时,表示输入终止,且该行不作处理。

输出格式

每组数据输出一个结果,每个结果占一行。

每个结果包含十个用空格隔开的数字,第一个数字表示 0 出现的次数,第二个数字表示 1 出现的次数,以此类推。

数据范围

0<a,b<100000000

输入样例

1 10
44 497
346 542
1199 1748
1496 1403
1004 503
1714 190
1317 854
1976 494
1001 1960
0 0

输出样例

1 2 1 1 1 1 1 1 1 1
85 185 185 185 190 96 96 96 95 93
40 40 40 93 136 82 40 40 40 40
115 666 215 215 214 205 205 154 105 106
16 113 19 20 114 20 20 19 19 16
107 105 100 101 101 197 200 200 200 200
413 1133 503 503 503 502 502 417 402 412
196 512 186 104 87 93 97 97 142 196
398 1375 398 398 405 499 499 495 488 471
294 1256 296 296 296 296 287 286 286 247

问题分析

求1~n中,x出现的次数:
例如:n=abcdefg , 求x在第4位出现的次数
分类讨论:
①前三位:000abc-1,x,后三位000999. 方案数:abc*1000
②前三位:abc,x
…2.1 d < x , 后三位:无解. 方案数:0
…2.2 d = x , 后三位:000~dfg. 方案数:dfg+1
…2.3 d > x , 后三位:000~999. 方案数:1000

注意:
1.当判断x在第1位出现的次数时,不存在情况① 2.当x=0且在分类①时,因为不能前导全0,因此得从001开始,(这一步特判即可)

代码

#include<bits/stdc++.h>
using namespace std;
const int N=10;
// 获取num数组从l到r的子数组组成的数字
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;
}

状态压缩DP

蒙德里安的梦想

问题描述

问题分析

摆放方块的时候,先放横着的,再放竖着的。总方案数等于只放横着的小方块的合法方案数。

如何判断,当前方案数是否合法? 所有剩余位置能否填充满竖着的小方块。可以按列来看,每一列内部所有连续的空着的小方块需要是偶数个。

用一个N位的二进制数,每一位表示一个物品,0/1表示不同的状态。因此可以用0→2N−1(N二进制对应的十进制数)中的所有数来枚举全部的状态。

状态表示
状态表示:f[i][j]表示已经将前 i -1 列摆好,且从第i−1列,伸出到第 i 列的状态是 j 的所有方案。其中j是一个二进制数,用来表示哪一行的小方块是横着放的,其位数和棋盘的行数一致。

状态转移
既然第 i 列固定了,我们需要看 第i-2 列是怎么转移到到第 i-1列的(看最后转移过来的状态)。假设此时对应的状态是k(第i-2列到第i-1列伸出来的二进制数,比如00100),k也是一个二进制数,1表示哪几行小方块是横着伸出来的,0表示哪几行不是横着伸出来的。

它对应的方案数是 f[i−1,k] ,即前i-2列都已摆完,且从第i-2列伸到第i-1列的状态为 k 的所有方案数。

这个k需要满足什么条件呢?

首先k不能和 j在同一行:因为从i-1列到第i列是横着摆放的1×2的方块,那么i-2列到i-1列就不能是横着摆放的,否则就是1×3的方块了!这与题意矛盾。所以 k和j不能位于同一行。

既然不能同一行伸出来,那么对应的代码为(k & j ) ==0 ,表示两个数相与,如果有1位相同结果就不是0, (k & j ) ==0表示 k和j没有1位相同, 即没有1行有冲突。

既然从第i-1列到第i列横着摆的,和第i-2列到第i-1列横着摆的都确定了,那么第i-1列 空着的格子就确定了,这些空着的格子将来用作竖着放。如果某一列有这些空着的位置,那么该列所有连续的空着的位置长度必须是偶数。

总共m列,我们假设列下标从0开始,即第0列,第1列……,第m-1列。根据状态表示f[i ] [j] 的定义,我们答案是什么呢? 返回定义处思考一下。答案是f[m][0], 意思是 前m-1列全部摆好,且从第m-1列到m列状态是0(意即从第m-1列到第m列没有伸出来的)的所有方案,即整个棋盘全部摆好的方案。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=12,M=1<<N;
int n,m;
long long f[N][M];
// 第一维表示列, 第二维表示所有可能的状态
bool st[M];
/*存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,
如果是偶数个零置为true。*/
int main()
{
    while(cin>>n>>m,n||m)
    {
        for(int i=0;i<1<<n;i++)
        {
            int cnt=0;
            st[i]=true;
            for(int j=0;j<n;j++)
                if(i>>j & 1)
                /*i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位; 
                 &1 为判断该位是否为1,如果为1进入if*/
                {
                    //这一位为1,看前面连续的0的个数,如果是奇数(cnt & 1为真)则该状态不合法
                    if(cnt & 1) st[i]=false;
                    cnt=0;
                }
                else cnt++;
            if(cnt & 1) st[i]=false;
        }
        memset(f,0,sizeof f);
        f[0][0]=1;
        for(int i=1;i<=m;i++)
            for(int j=0;j<1<<n;j++)
                for(int k=0;k<1<<n;k++)
                	//看第i-2列伸出来的和第i-1列伸出去的是否冲突
                	//这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的
                	//第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
               		 //那么合在第i-1列,到底有多少个1呢?
                    if((j & k)==0 && st[j | k])
                        f[i][j]+=f[i-1][k];
        cout<<f[m][0]<<endl;
    }
    
    return 0;
}

最短Hamilton路径

问题描述

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

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

输入格式

第一行输入整数 n。

接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j])。

对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]。

输出格式

输出一个整数,表示最短 Hamilton 路径的长度。

数据范围

1≤n≤20,0≤a[i,j]≤107

输入样例

5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0

输出样例

18

问题分析

用二进制来表示要走的所以情况的路径,这里用i来代替
例如走0,1,2,4这三个点,则表示为:10111;
走0,2,3这三个点:1101;
状态表示:f[i][j];
集合:所有从0走到j,走过的所有点的情况是i的所有路径
属性:MIN
状态计算:如1中分析一致,0–>·····–>k–>j中k的所有情况
状态转移方程:f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j])

代码

#include<bits/stdc++.h>
using namespace std;
const int N=20,M=1<<N;
int n;
int w[N][N];
int f[M][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[0][0]=0;
    for(int i=0;i<1<<n;i++) //i表示所有的情况
        for(int j=0;j<n;j++)//j表示走到哪一个点
            if(i>>j & 1)
                for(int k=0;k<n;k++)//k表示走到j这个点之前,以k为终点的最短距离
                    if(i>>k & 1) // 用于判断一个数的二进制表示中第k位是否为1
                        f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
    cout<<f[(1<<n)-1][n-1]<<endl;
    return 0;
    
}

树形DP

没有上司的舞会

问题描述

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

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

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

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

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

输入格式

第一行一个整数 N。

接下来 N 行,第 i 行表示 i 号职员的快乐指数 Hi。

接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。(注意一下,后一个数是前一个数的父节点,不要搞反)。

输出格式

输出最大的快乐指数。

数据范围

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

输入样例

7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

输出样例

5

问题分析

本题稍加思索即可得出为树形dp。

每个人只有两种状态,则设dp[0][i]为第i个人不来,他的下属所能获得的最大快乐值;dp[1为第i个人来,他的下属所能获得的最大快乐值。

所以容易推出状态转移方程:

dp[0][i]=∑u=sonsmax(dp[1][u],dp[0][u])

当前节点不选,那么子节点随意

dp[1][i]=∑u=sonsdp[0][u]+happy[i]

当前节点选,子节点不能选

分析可得,每个人的状态要在下属的状态更新完了才能更新,所以用类似拓扑的方法,只记录每个子节点的父亲,最后从所有入度为0

的点开始跑就行了。在更新每个子节点时顺便让父节点加上自己的权值,最后访问父节点时权值已经更新好了,就可以省去建图的麻烦。
在这里插入图片描述

代码

#include<bits/stdc++.h>
using namespace std;
const int N=6010;
int n;
int h[N],e[N],ne[N],idx;
int happy[N];
int f[N][2];
int has_father[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][1]+=f[j][0];
        f[u][0]+=max(f[j][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;
        add(b,a);
        has_father[a]=true;
    }
    int root=1;
    while(has_father[root]) root++;
    dfs(root);
    cout<<max(f[root][0],f[root][1]);
    return 0;
}

记忆化搜索

滑雪

问题描述

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

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

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

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

下面给出一个矩阵作为例子:

1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。

在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 25 个区域。

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

输入格式

第一行包含两个整数 R 和 C。

接下来 R 行,每行包含 C 个整数,表示完整的二维矩阵。

输出格式

输出一个整数,表示可完成的最长滑雪长度。

数据范围

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

输入样例

5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

输出样例

25

问题分析

在这里插入图片描述
在这里插入图片描述

代码

#include<bits/stdc++.h>
using namespace std;
const int N=310;
int n,m;
int f[N][N];
int h[N][N];
int dx[]={-1,0,1,0},dy[]={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[a][b]<h[x][y])
            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<<endl;
    return 0;
}
  • 19
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值