算法基础课(acwing):区间,计数,数位DP

文章讲述了区间动态规划在两个问题中的应用:合并相邻石子堆以求最小代价和计算整数划分的方案数,介绍了状态转移方程和相应的代码实现。

1.区间DP

区间DP顾名思义就是数组集合f[i][j]表示的是 i ~ j 区间之间的集合关系

例题——石子合并:

设有 N 堆石子排成一排,其编号为 1,2,3,…,N

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 44 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 44,得到 4 5 2, 又合并 1、2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;

如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

输入样例:

4
1 3 5 2

输出样例:

22

 算法思路及其代码:

这道题是一道典型的区间DP问题,f[i][j]表示的集合是i~j中的堆合并成一个堆的所有方案,返回的是这些方案的代价最小值

既然只能相邻的两堆合并,那么f[i][j] 的i~j的区间就可以用k(i<k<j)来分成f[i][k]和f[k+1][j],然后让这两个堆合并成一个堆。

那f[i][j]怎么具体怎么求呢?f[i][k]是返回i~k合并成一堆的最小代价,f[k+1][j]返回的是k+1~j的合并成一堆的最小代价,f[i][j]是不是就是f[i][k]合并成一堆的最小代价 + f[k+1][j]合并成一堆的最小代价 再加上这两个堆合并的代价(就两个堆,两个堆合并成一个堆的代价就是两个堆里所有质量之和)

我们用前缀和来存储1~i中所有堆质量之和,s[i]就表示1~i中所有堆质量之和,那i~j中质量之和就是s[j]-s[i-1]

所以状态转移方程可以写成 f[i][j] = f[i][k] + f[k+1][j]+ s[j]-s[i-1]

我们用len来表示区间长度,len的长度从2开始,因为len=1的话区间长度只有1就是只有一个数

#include <iostream>
#include <algorithm>

using namespace std;

const int N=310;

int n;
int s[N];
int f[N][N];//从i到j区间中所有合并堆方式的集合,返回最小的代价

int main(){
    cin>>n;
    
    for(int i=1;i<=n;i++)cin>>s[i];
    
    for(int i=1;i<=n;i++)s[i]+=s[i-1];
    for(int len=2;len<=n;len++){
        for(int i=1;i+len-1<=n;i++){
            int j=i+len-1;
            f[i][j]=1e8;
            for(int k=i;k<=j-1;k++){
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
            }
        }
    }
    cout<<f[1][n];
    
    return 0;
}

计数类DP

计数类dpf[i][j]存的是集合的数量或者是某种数量

例题——整数划分:

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

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

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

输入样例:

5

输出样例:

7

算法思路及其代码 :

假设n=5,那么n可以分成5 ,4 1 ,3 2,3 1 1,2 2 1,2 1 1 1 ,11111总共7类

我们发现每一类不论是否打乱顺序都算作一类,如 221和122是一类,只不过顺序变了一下,

那我们就可以用完全背包问题的思路来做,f[i][j]表示的是从1~i中选,每个数可以选任意多次,只要他们之和恰好等于j的集合,返回的是集合的数量 ,完全背包问题选择的方案是不重的,如选了221就不会有212了,这样也正好只用算一次。

这样我们就可以按照完全背包的模式求状态转移方程了

我们按照 选 i 和 不选 i 来分类

不选i: f[i][j]=f[i-1][j]

选 i :f[i][j] = f[i-1][j]+f[i-1][j-i] + f[i-1][j-2*i] + ..... f[i-1][j-K*i] +...

       f[i][j-i]=            f[i-1][j-i] + f[i-1][j-2*i]+.........f[i-1][j-K*i] + ....

这样状态转移方程就可以写成 f[i][j]=f[i-1][j]+f[i][j-i]

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N=1010,mod=1e9+7;

int n;
int f[N][N];//1~i中恰好等组成j的所有选法的集合,返回选法的数量

int main(){
    cin>>n;
    
    for(int i=1;i<=n;i++)f[i][0]=1;
    
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            f[i][j]=f[i-1][j];
            if(j>=i)f[i][j]=(f[i][j]+f[i][j-i])%mod;
        }
    }
    
    cout<<f[n][n];
    
    return 0;
}

优化成一维:

#include <iostream>
#include <algorithm>

using namespace std;

const int N=1010,mod=1e9+7;

int n;
int f[N];//1~i中恰好等组成j的所有选法的集合,返回选法的数量

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];
    
    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 出现的次数,以此类推。

输入样例:

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出现的次数,我们就定义这个函数是count(n,x),返回的是1~n中x出现的次数。那a~b中所有x出现的次数怎么表示呢,是不是可以用count(b,x) - count(a-1,x)来表示a~b中出现的次数。

好了,我们再把重心放在怎么表示count(n,x)上,怎么计算1~n中x出现的次数呢?

假设 n 这个数是六位的 每一位表示成 a b c d e f

假设x=3,那就是返回1~n中3出现的次数,那3可能出现在a这一位,b这一位....一直到f这一位。

我们可以先统计x=3在1~n中 c 这一位出现的次数,那就是统计1~n中所有数在c这一位出现x=3的次数,然后将 所有位 x出现的次数加起来就是1~n中x出现的次数

假如统计x在1~n中c这一位出现的次数

第一步是:统计 前两位如果小于ab的所有数 x出现的次数,那数可以取: 00~ab-1  c 000~999(c这一位前面ab可以取 00~ab-1种方式,c后面可以取000~999) 这些数的个数就是x在c这一位出现的次数,次数之和为ab*1000(前面ab种选法,后面1000种选法)

第二部步:统计前两位如果等于ab的所有数,那又要分三种情况:(1)如果 c<x的话,说明1~abcdef的数 根本就取不到x,就0次(2)如果c==x的话,说明还有abc  000~def 这些数中包含x,次数为def+1次(3)如果c>x的话,那abc 000~999 这些数中包含x,次数为1000次

把两种统计次数加起来就是1~n种所有数在c这一位出现x=3的次数,但是还要考虑一些特殊情况:

(1)当统计最高位的时候,假如统计x在最高位出现的次数,只需要求上面的第二步统计就好了,不需要求第一步,因为没有比最高位还高的位了......

(2)当统计x=0在1~n中出现的个数时,第一步的统计只能从01开始,不能从00开始,所以当x=0的时候,第一步要减去统计00的部分。

总的来说,就是要注意 统计每个数(0~9)的时候,统计最高位只需要第二步。统计x=0的时候,第一步要从01开始统计(01 ~ab-1  0  000~999)

代码

#include <iostream>
#include <algorithm>
#include <vector>


using namespace std;

int get(vector<int>nums,int l,int r){//求数字n:l~r之间的数是多少
    int res=0;
    for(int i=r;i>=l;i--){
        res=res*10+nums[i];
    }
    return res;
}

int power10(int x){//求10的x次方
    int res=1;
    while(x--){
        res*=10;
    }
    return res;
}

//count(n,x)返回的是1~n中x出现的数量
int count(int n,int x){
    if(!n)return 0;
    
    vector<int>nums;
    int res=0;
    
    while(n){
        nums.push_back(n%10);
        n/=10;
    }
    
    n=nums.size();
    
    for(int i=n-1-!x;i>=0;i--){//如果x是0的话从第二位开始算,如果不是0的话从最高位开始算
        
        if(i<n-1){//如果算最高位的话就不需要这一步了,如果算其他位就需要这一步
            res+=get(nums,i+1,n-1)*power10(i);
            if(x==0)res-=power10(i);
        }
        
        if(nums[i]==x)res+=get(nums,0,i-1)+1;
        else if (nums[i]>x)res+=power10(i);
    }
    
    return res;
}

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

写在后面:好难!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值