【AcWing算法基础班】动态规划(三)学习笔记

一、数位统计DP

例 计数问题

给定两个整数a和b,求a和b之间的所有数字中0~9出现的次数,如a=1,b=12,1 2 3 4 5 6 7 8 9 10 11 12,1共出现5次。
分情况讨论:记count(n,x)为1至n中x的出现次数
则a至b中x的出现次数是count(b,x)-count(a-1,x),类似前缀和。
如果要求1在1至n中出现的次数,设n=abcdefg,要分别求1在每一位上出现的次数,如求1在第4位上出现的次数,即求有多少个形如xxx1yyy的数在1至abcdefg之间。分类讨论1<=xxx1yyy<=abcdefg:
①xxx=0至abc-1,yyy可以取000至999,共1000×abc种选法
②xxx=abc时,分三种情况:
Ⅰ 若d<1,即d=0时,xxx1yyy>abcdefg,共0种选法
Ⅱ 若d=1,yyy=000至efg,共efg+1种选法
Ⅲ 若d>1,yyy=000至999,共1000种选法
注意:最高位取0的情况与abc取000的情况要特别拿出来讨论一番,因为我们要求最高位的数不能是0,如果取0显然无意义,会产生重复计算。
时间复杂度:O(1)
AC代码(附详细注释):

#include<iostream>
#include<algorithm>
#include<math.h>
using namespace std;
int cal_len(int n) {//计算整数n有多少位
	int ans = 0;
	while (n)ans++, n /= 10;
	return ans;
}
int cal(int n, int i) {//计算从1到n的整数中数字i出现多少次
	int ans = 0, len = cal_len(n);
	for (int j = 1; j <= len; j++) {//计算从右到左第j位上的数字出现的次数
		//abcdefg,设第j位上的数是d,l是abc,r是efg,p是位权
		int p = pow(10, j - 1);
		int l = n / (10 * p), r = n%p, d = (n / p) % 10;
		//计算第j位左边的整数小于l的情况,xxx=000至abc-1,如三位时yyy=000至999
		//且d必能取到i
		if (i)ans += (l*p);
		//如果i=0,左边高位不能全为0,xxx=001至abc-1
		//左边高位全为0时,i自己就是最高位,然而0不能作为最高位,否则会重复计算
		if (!i&&l)ans += (l - 1)*p;
		//计算第j位左边的整数等于l
		//d>i,yyy=000至999
		//若i和l都不为0,是一般情况
		//若i==0,l!=0,这种情况也是有意义的
		//若i!=0,l==0,最高位i不为0,有意义
		//若i==0,l==0,最高位i是0,0不能作为最高位,否则会重复计算,无意义
		if ((d > i) && (i || l))ans += p;
		//d==i,yyy=000至abc
		//若i==0,l==0,说明最高位d是0,而len统计的就是不为0的最高位,这样的数据不存在
		//因此不用再加上i||l的条件
		if (d == i)ans += (r + 1);
	}
	return ans;
}
int main()
{
	int a, b;
	while (cin >> a >> b && (a||b)) {
		if (a > b)swap(a, b);//保证a比b小
		for (int i = 0; i <= 9; i++) {
			cout << cal(b, i) - cal(a - 1, i) << " ";
		}
		cout << endl;
	}
    return 0;
}

二、状态压缩DP

例1 蒙德里安的梦想

求把N×M的棋盘分割成若干个1×2的长方形,有多少种方案?
思路:当我们把所有横向小方格放完的时候,如果能将棋盘填满,我们的纵向小方格一定只有一种情况放进去,即横向小方格的放法数等于纵向小方格的放法数。
状态表示f[i][j],属性:只枚举横向放置的1×2小方格,求所有摆到了第i列,且由第i列伸出来到第i+1列,使得第i+1列的状态是j的情况下总共的方案数。二进制表示第i-1列的格子占用情况,占用为1,不占为0,表示为二进制数。
要算第i列的某个状态,则枚举第i-1列的所有状态,但是需要判断能否从这一状态转移过来,需要满足条件:
①设第i列对应的上一列的状态是j,第i-1列对应的上一列的状态是k,显然两个横向小方格不能重叠,即j与k在二进制的任意一位都不能同时为1,故(j&k)==0。注意1×2方格头部在第i列,尾部在第i-1列,要判断能否放置它。
②所有竖直方向的连续空白小方块必须是偶数的,即第i-1列中不能出现连续奇数个0。为什么要看i-1列,是由于对于横向放置的方格,确定了第i-1列有没有连接前一列后,才可以确定第i列能否连接第i-1列。如果第i-1列中没有连续奇数个0,它才可以将这种状态转移到第i列。第i-2列有,则第i-1列有,故等价于(1|0)=1,即(j|k)==0的位置就是第i-1列中的空白位置,故应判断j|k是否不存在连续奇数个零
状态计算:f[i][j]=Σf[i-1][k],k满足条件:(j&k)==0,(j|k)==0无连续奇数0
答案是:f[M][0],M是棋盘的列数,表示从第M列伸出来到第M+1列使得第M+1列的状态只有可能是0才能恰好完成分割。同理,f[0][0]=1。
方法:枚举所有列,下一列的所有状态以及上一列的所有状态,满足条件即可完成状态转移。
预处理关于判断(j|k)==0有无连续奇数0的方法:直接预判断每一列,统计这一列的n行中,即二进制的每一位上的数。一段连续0的数量为奇数就置否。
注意开long long。
AC代码:

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=1<<15;
bool st[maxn];
long long f[15][maxn];
int main(){
    int N,M;
    while(cin>>N>>M,N||M){
        for(int i=0;i<(1<<N);i++){
            st[i]=true;
            int cnt=0;
            for(int j=0;j<N;j++){
                if((i>>j)&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++){
                    if((j&k)==0 && st[j|k])f[i][j]+=f[i-1][k];
                }
            }
        }
        cout<<f[M][0]<<endl;
    }
    return 0;
}

例2 最短哈密顿路径

给定一张n个点的带权无向图,点从0至n-1标号,求起点0到终点n-1的最短哈密顿路径。哈密顿路径:从0至n-1不重不漏地恰好经过每个点一次
①朴素考虑:n n-1 n-2 n-3 …… 1 即n!种选法,对于每种选法,路径长度为n,故暴力做法的时间复杂度为n!×n,非常高,不可取。
②状态表示f[i][j],集合:所有从起点0走到终点j,走过的所有点是i的所有路径。如0到6有7个点,取1110011这样的二进制数表示走过的所有点的状态。二进制数的每一位表示当前这个点是否走过了。1110011表示,第0个点走过了,第1个点走过了,第2个点没走过,第3个点没走过,第4个点走过了,第5个点走过了,第6个点走过了。由于最多有20个点,因此需要开20位存储,最大为2^20-1,即1048575,完全可以。
属性:从0到j的所有路径中的最小长度
集合划分:比如终点是j,枚举倒数第二个点是哪个点,即当前终点j是由之前的哪一个点走过来的。倒数第二个点可能是0、1、2、3、……、n-1,即根据倒数第二个点是哪个点来分类。设倒数第二个点是k,也就是说从0先走到了k,最后一步再从k走到j。最后一步走过的边权是w[k][j]已知,那么我们的目标是让0到k的路径最短。0到k经过的所有点一定没有j,因为每个点只能走一次。
状态计算:f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]),k=0,1,2,3,……,n-1
初始条件:f[1][0]=0,其余正无穷
转移过程:如果终点j走到了,就可以枚举所有的k。将i这一状态的第j位置0得到的倒数第二个状态中如果包含k,就满足上述状态计算方程。
答案:f[(1<<n)-1][n-1]
AC代码:

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=20,M=1<<20;
int w[N][N];
int dp[M][N];
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            cin>>w[i][j];
        }
    }
    memset(dp,0x3f,sizeof(dp));
    dp[1][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){
                        dp[i][j]=min(dp[i][j],dp[i-(1<<j)][k]+w[k][j]);
                    }
                }
            }
        }
    }
    cout<<dp[(1<<n)-1][n-1]<<endl;
    return 0;
}

三、树形DP

例 没有上司的舞会

大学有N名职员,编号1至n。父节点是子节点的直接上司,每个职员有一个快乐指数Hi。没有职员愿意和直接上司参加舞会。主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求最大值。
状态表示:
f[u][0]:从所有以u为根的子树中选择,并且不选u这个点的方案
f[u][1]:从所有以u为根的子树中选择,并且选择u这个点的方案
属性:快乐指数总和在所有方案中的最大值
状态计算,设u有s1、s2、……、skmax共kmax个子节点,则:
f[u][0]+=Σmax(f[sk][0],f[sk][1]),k=1,2,……,kmax
f[u][1]+=Σf[sk][0] ,k=1,2,……,kmax
时间复杂度:O(n),与所有边的数量相同,遍历搜边即可
实现:用vector静态邻接表存储边集关系,布尔数组判断每个节点是否有父节点,没有父节点的就是根节点,即dfs的起点,先下行将点初始化,再上溯进行两种状态的dp计算。
答案:max(dp[root][0],dp[root][1])
AC代码:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn = 1e4 + 5;
vector<int> son[maxn];
int n;
int happy[maxn];
bool has_fa[maxn];
int dp[maxn][2];
void dfs(int now) {
	dp[now][0] = 0;
	dp[now][1] = happy[now];
	for (int i = 0; i < son[now].size(); i++) {
		int next = son[now][i];
		dfs(next);
		dp[now][0] += max(dp[next][0], dp[next][1]);
		dp[now][1] += dp[next][0];
	}
}
int main()
{
	cin >> n;
	for (int i = 1; i <= n; i++)cin >> happy[i];
	int a, b;
	for (int i = 1; i < n; i++) {
		cin >> a >> b;
		has_fa[a] = true;
		son[b].push_back(a);
	}
	int root = 0;
	for (int i = 1; i <= n; i++) {
		if (!has_fa[i]) { root = i; break; }
	}
	dfs(root);
	int ans = max(dp[root][0], dp[root][1]);
	cout<<ans<<endl;
    return 0;
}

四、记忆化搜索

例 滑雪

给定R行C列的矩阵为滑雪场,矩阵中的点(i,j)表示滑雪场中该点位置的高度。一个人从滑雪场中的某个区域出发,每次向上下左右任意一个方向移动一个单位距离,前提是前往的高度低于自己目前所在区域的高度。找出最长滑雪轨迹,并输出长度。
状态表示f[i][j],集合:所有从(i,j)开始滑的路径
属性:这些路径中有着最长轨迹的那条路径的长度
集合划分:分别向上、下、左、右四个方向滑动,路径中没有环
向右:f[i][j+1]+1
向左:f[i][j-1]+1
向上:f[i-1][j]+1
向下:f[i+1][j]+1
状态计算:f[i][j]=max(f[i][j+1],f[i][j-1],f[i-1][j],f[i+1][j])+1
过程:求出从每个点出发的最长状态后,枚举求出从每个点出发的全部状态中的最长状态
语法特性:&v=f[x][y],再C++中表示v等价于f[x][y],即v是后者的引用。
初始化:v的最小值为1
注意:每次遍历时,每个点只需要计算一遍,因此如果当前点的值不为初始值-1,说明计算过了,就不需要再去计算,直接返回算好的值即可。
剪枝:高度递减,不可能形成闭环,每个点只需要计算一遍。
AC代码:

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=305;
int dp[maxn][maxn];
int mp[maxn][maxn];
int R,C;
int dir[4][2]={{0,1},{1,0},{-1,0},{0,-1}};
int dfs(int x, int y){
    int &v=dp[x][y];
    if(v!=-1)return v;
    v=1;
    for(int i=0;i<4;i++){
        int nx=x+dir[i][0];
        int ny=y+dir[i][1];
        if(nx>=1&&nx<=R&&ny>=1&&ny<=C&&mp[nx][ny]<mp[x][y]){
            v=max(v,dfs(nx,ny)+1);
        }
    }
    return v;
}
int main(){
    cin>>R>>C;
    for(int i=1;i<=R;i++){
        for(int j=1;j<=C;j++){
            cin>>mp[i][j];
        }
    }
    memset(dp,-1,sizeof(dp));
    int ans=0;
    for(int i=1;i<=R;i++){
        for(int j=1;j<=C;j++){
            ans=max(ans,dfs(i,j));
        }
    }
    cout<<ans<<endl;
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

keguaiguai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值