动态规划

01背包

01背包是动态规划里最经典的一个问题
题目:有 N 件物品和一个容量为 V 的背包。放入第 i 件物品耗费的费用是
Ci,得到的价值是 Wi。求解将哪些物品装入背包可使价值总和最大。

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放戒
丌放。用子问题定义状态:即 F[i, v] 表示前 i 件物品恰放入一个容量为 v
的背包可以获得的最大价值。则其状态转移方程便是:
F[i, v] = max{F[i-1, v], F[i-1, v-Ci] + Wi}

此时转移方程为:F[v]=max(F [v] , F[v-Ci]) 

 

P1060 开心的金明

https://www.luogu.org/problemnew/show/P1060

  这题就是01背包板子题,最开始RE了,发现是因为数组范围开小了。以后要注意RE的话可能是除以0,或者数组范围开太小或太大。

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int n,m;//总钱数,个数
int v[30],p[30];//单价,重要度
int dp[30005];
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&v[i],&p[i]);
    }
   // dp[0]=0;
    for(int i=1;i<=m;i++)
    {
        for(int j=n;j>=v[i];j--)
        {
            dp[j]=max(dp[j],dp[j-v[i]]+v[i]*p[i]);
        }
    }
    cout<<dp[n]<<endl;
    return 0;
}

 

 

常见dp模型

P1280 尼克的任务

https://www.luogu.org/problemnew/show/P1280

即设ans[i]表示i~n的最大空闲时间 

(本时刻无任务)ans[i]=ans[i+1]+1;//继承上一个时刻的最大空闲时间后+1

(本时刻有任务)ans[i]=max(ans[i],ans[i+a[q].t])//ans[i+a[q].t]表示在这个时刻的任务的结束时间,找出选择哪一个本时刻任务使空闲时间最大化

那么既然是倒着搜,从后往前的任务对应的开始时间自然也要反过来,从大到小排序(同时也是为了把相同开始时间的任务放到一起),当然在进行状态刷新的时候别忘了拿q不断计一下已经到哪一个任务了

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll maxn=10010;
ll n,k,q=1;
ll b[maxn],ans[maxn];
struct node
{
    ll p,t;
} a[maxn]; //用结构体排序方便

bool cmp(node a,node b)
{
    return a.p>b.p;    //因逆推,所以从大到小
}
int main()//开始了
{

    scanf("%lld %lld",&n,&k);
    for(ll i=1; i<=k; i++)
    {
        scanf("%lld %lld",&a[i].p,&a[i].t);
        b[a[i].p]++;//b数组求了以a[i].p的开始时间的任务数
    }
    sort(a+1,a+k+1,cmp);//排序
    for(ll i=n; i>0; i--) //注意是逆推{
        if(b[i]==0)//说明本时刻没有任务,所以空闲时间为继承下一时刻的空闲时间+1
            ans[i]=ans[i+1]+1;
        else//本时刻有任务
        {
            for(ll j=1; j<=b[i]; j++)//注意b[i]是以i时间开始的任务的任务数量!!!
            {
                if(ans[i+a[q].t]>ans[i])//更新最大空闲时间,i+a[q].t算的就是结束时间
                    ans[i]=ans[i+a[q].t];
                q++;
            }
        }

//解方程
    printf("%lld",ans[1]);//结果存在第一位,输出
    return 0;
}

P1164 小A点菜

https://www.luogu.org/problemnew/show/P1164

①if(j==第i道菜的价格)f[i][j]=f[i-1][j]+1;方案数就为前i-1道菜花光了j元钱加上第i道菜花的这j元钱 的这两种情况
 ②if(j>第i道菜的价格) f[i][j]=f[i-1][j]+f[i-1][j-第i道菜的价格];
 就说明这个钱不可能是第i道菜全花的,有可能前i-1道菜花光了j元,也有可能前i-1道菜花了(j-第i道菜的价格)     
③if(j<第i道菜的价格) f[i][j]=f[i-1][j];说明这j元一定没有花在第i道菜身上


 说的简单一些,这三个方程,每一个都是在吃与不吃之间抉择。若钱充足,办法总数就等于吃这道菜的办法数与不吃这道菜的办法数之和;若不充足,办法总数就只能承袭吃前i-1道菜的办法总数。依次递推,在最后,我们只要输出f[n][m]的值即可。       

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int a[105];
int n,m;
int f[101][10001];//定义f[i][j]为用前i道菜用光j元钱的办法总数
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&a[i]);
    }
    for(int i=1; i<=n; ++i)
        for(int j=1; j<=m; ++j)
        {
            //if(j==第i道菜的价格)f[i][j]=f[i-1][j]+1;
            //方案数就为钱i-1道菜花光了j元钱加上第i道菜花的这j元钱
            if(j==a[i])
                f[i][j]=f[i-1][j]+1;
            //if(j>第i道菜的价格) f[i][j]=f[i-1][j]+f[i-1][j-第i道菜的价格];
            //就说明这个钱不可能是第i道菜全花的,有可能前i-1道菜花光了j元,也有可能前i-1道菜花了(j-第i道菜的价格)
            if(j>a[i])
                f[i][j]=f[i-1][j]+f[i-1][j-a[i]];
            //if(j<第i道菜的价格) f[i][j]=f[i-1][j];
            //说明这j元一定没有花在第i道菜身上
            if(j<a[i])
                f[i][j]=f[i-1][j];
        }
    cout<<f[n][m]<<endl;
    return 0;
}

P1091 合唱队形

https://www.luogu.org/problemnew/show/P1091

 思路:

求到 i 位置为止的最长上升子序列和自 i 位置向后的最长下降子序列
相加减一取最大值

注意求自 i 位置向后的最长下降子序列的方法

 for(int i=n;i>=1;i--)
    {
        dp2[i]=1;
        for(int j=i+1;j<=n;j++)
        {
            if(a[j]<a[i]&&dp2[i]<dp2[j]+1)
            {
                dp2[i]=dp2[j]+1;
            }
        }
    }

AC代码如下: 

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
//到这一位置为止的最长上升子序列和自这个位置向后的最长下降子序列
//相加减一取最大值
int n;
int a[105];
int dp1[105],dp2[105];
int main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&a[i]);
    }
    for(int i=1; i<=n; i++)
    {
        dp1[i]=1;
        for(int j=1; j<i; j++)
        {
            if(a[j]<a[i]&&dp1[i]<dp1[j]+1)//到这一位置为止的最长上升子序列
            {
                dp1[i]=dp1[j]+1;
            }
        }
    }
    int ans=0;
    for(int i=n;i>=1;i--)
    {
        dp2[i]=1;
        for(int j=i+1;j<=n;j++)
        {
            if(a[j]<a[i]&&dp2[i]<dp2[j]+1)
            {
                dp2[i]=dp2[j]+1;
            }
        }
    }
    for(int i=1;i<=n;i++)
    {
        ans=max(ans,dp1[i]+dp2[i]-1);
    }
    cout<<n-ans<<endl;
    return 0;
}

p1020导弹拦截

(求一个不上升序列长度和一个上升序列长度) 

https://www.luogu.org/problemnew/show/P1020

要学会O(nlongn)的方法。

#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int a[N],d1[N],d2[N],n;
int main()
{
    while(cin>>a[++n]);
    n--;     //输入
    int len1=1,len2=1;      //初始长度为1
    d1[1]=a[1];     //用于求不上升序列长度
    d2[1]=a[1];     //用于求上升序列长度
    for(int i=2; i<=n; i++)         //从a[2]开始枚举每个数(a[1]已经加进去了)
    {
        if(d1[len1]>=a[i])d1[++len1]=a[i];      //如果满足要求(不上升)就加入d1
        else        //否则用a[i]替换d1中的一个数
        {

            int p1=upper_bound(d1+1,d1+1+len1,a[i],greater<int>())-d1;
            d1[p1]=a[i];

        }
        if(d2[len2]<a[i])d2[++len2]=a[i];       //同上,如果当前数保持上升
        else
        {
            int p2=lower_bound(d2+1,d2+1+len2,a[i])-d2;
            d2[p2]=a[i];
        }
    }
    cout<<len1<<endl<<len2;     //输出
    return 0;      //结束
}

https://nanti.jisuanke.com/t/A1634 计蒜客A1634划分整数

题意:把一个正整数n分解成不多于k个正整数相加,问一共有多少种分解方式

注意:一定要用ll,慎用int,经常就会因为用int导致答案错误,所以最好养成用ll的习惯。

其次:这道题推状态转移方程,是我苦恼了很久的点。

当n>k时:可以分为恰好分成k个,和分成不多于k-1个这两种情况,第二种情况是dp[n,k-1],第一种就是把每一个数减一可以转化为把n-k分为不多于k个数的情况(因为第一种是恰好分为k份,所以每份至少一,所以就相当于还剩n-k再分在这k份上的情况)

#include <iostream>
#include<bits/stdc++.h>
/*蒜头君特别喜欢数学。今天,蒜头君突发奇想:如果想要把一个正整数 n 分解成
不多于 k 个正整数相加的形式,那么一共有多少种分解的方式呢?

蒜头君觉得这个问题实在是太难了,于是他想让你帮帮忙*/
using namespace std;
#define ll long long
ll dp[305][305];
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=k;j++)
        {
            if(j>i)
                dp[i][j]=dp[i][i];
            else if(i==j)
                dp[i][j]=dp[i][j-1]+1;
            else
            {
                dp[i][j]=dp[i][j-1]+dp[i-j][j];
            }
        }
    }
    cout<<dp[n][k]<<endl;
    return 0;
}

背包问题

p1478陶陶摘苹果https://www.luogu.org/problemnew/show/P1478

题意:苹果树上的每个苹果,会告诉苹果的高度已经主人摘它需要的最大力气。a是板凳高度,b是陶陶能伸的最大高度。题目告诉了开始时陶陶的力气s,求最多能摘多少苹果。

这道题就类似于01背包

#include<iostream>
using namespace std;
int dp[5005][1001];
int xi[5005],yi[5005],n,s,a,b;
int main(){
    cin>>n>>s>>a>>b;
    for(int i=1;i<=n;i++){
        cin>>xi[i]>>yi[i];
    }
    for(int i=1;i<=n;i++)//枚举考虑每一个苹果
    for(int j=0;j<=s;j++){//枚举背包大小
        dp[i][j]=dp[i-1][j];//不能取就直接转移考虑之前苹果的最大值
        if(xi[i]<=a+b&&j>=yi[i])//如果能够取
        dp[i][j]=dp[i-1][j-yi[i]]+1>dp[i][j]?dp[i-1][j-yi[i]]+1:dp[i][j];//这个就是动态转移方程。max函数运行太慢,我们这里选择三目运算符取较大值
    }
    cout<<dp[n][s];//因为是从前向后递推,因此接收最终答案的位置也从最前面转到了最后面
    return 0;
}

 

数位dp

1、数位dp模板等总结https://blog.csdn.net/wust_zzwh/article/details/52100392

计算出从n~m的数中,不包含4、62(必须连续)的数有哪些  数位dp模板题

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll a[100];
ll dp[20][2];
ll n,m;
ll dfs(ll index,bool flag6,bool limit)//flag6代表上一位是否为6
{
    if(index==-1)
        return 1;
    if(!limit&&dp[index][flag6]!=-1)
        return dp[index][flag6];
    int up=limit?a[index]:9;
    ll ans=0;
    for(int i=0;i<=up;i++)
    {
        if(i==4||(i==2&&flag6))
            continue;
        ans+=dfs(index-1,i==6,limit&&i==a[index]);
    }
    if(!limit)
        dp[index][flag6]=ans;
    return ans;
}
ll solve(ll x)
{
    ll pos=0;
    while(x)
    {
       a[pos++]=x%10;
       x/=10;
    }
    return dfs(pos-1,0,1);
}
int main()
{
    while(scanf("%lld%lld",&n,&m)!=EOF&&(n||m))
    {
        memset(dp,-1,sizeof(dp));
        printf("%lld\n",solve(m)-solve(n-1));
    }


    return 0;
}

https://www.luogu.org/problemnew/show/P1980 p1980计数问题,求在1~n间数字x一共出现了多少次。

//2.统计0~9在1~n各出现了多少次
#include<cstdio>
#include<cstring>
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 15;
ll f[N][2][N][2];
int num[N];  //num来存这个数每个位子上的数码
/*
记忆化搜索。
len是当前为从高到低第几位。issmall表示当前位是否和num[len]相等,0是相等,1是不相等。
sum表示当前数字出现的次数。zero表示之前是否是前导0。d是当前在算的数码。
*/
ll dfs(int len, bool issmall, int sum, bool zero, int d)
{
    ll ret = 0;
    if (len == 0) return sum;  //边界条件
    if (f[len][issmall][sum][zero] != -1) return f[len][issmall][sum][zero];  //记忆化
    for (int i = 0; i < 10; i ++){
        if (!issmall && i > num[len]) break;
        /*
        由于我们是从高位到低位枚举的,所以如果之前一位的数码和最大数的数码相同,这一位就只能枚举到num[len];
        否则如果之前一位比最大数的数码小,那这一位就可以从0~9枚举了。
        */
        ret += dfs(len-1, issmall || (i<num[len]), sum+((!zero || i) && (i==d)), zero && (i == 0), d);
        /*
        继续搜索,数位减一,issmall的更新要看之前有没有相等,且这一位有没有相等;
        sum的更新要看之前是否为前导0或者这一位不是0;
        zero的更新就看之前是否为前导0且这一位继续为0;
        d继续传进去。
        */
    }
    f[len][issmall][sum][zero] = ret;
    //记忆化,把搜到的都记下来
    return ret;
}
ll solve(ll x, int d)
{
    int len = 0;
    while (x){
        num[++ len] = x%10;
        x /= 10;
    } //数字转数位
    memset(f, -1, sizeof f); //初始化
    return dfs(len, 0, 0, 1, d); //开始在第len位上,最高位只能枚举到num[len]所以issmall是0,sum=0,有前导0。
}
int main()
{
    ll n; //注意都要开long long
    int x;
    scanf("%lld%d",&n,&x);
    cout<<solve(n,x)<<endl;
    /*while(~scanf("%lld", &k))
    {
         for (int i = 0; i < 10; i ++)
            printf("%lld\n", solve(k, i));
    }*/
    return 0;
}

poj3252https://vjudge.net/contest/300233#problem/E

求n~m的数中满足二进制的0的个数大于等于1的共有多少个数。  

注意在solve函数中,不用x>>=1,这样会溢出,还是用x/=2吧。

dp[pos][num0][num1]代表从高到低第pos位时,0的个数为num0,1的个数为num1,在后面任意填时该状态下的总个数。

数位dp都是从高位到低位看的

#include <iostream>
#include<cstring>
#include<cstdio>
using namespace std;
#define ll long long
ll a[50];
ll dp[50][40][40];
//dp[pos][num0][num1],代表从高到低第pos位时,0的个数为num0,1的个数为num1,在后面任意填时该状态下的总个数。
ll dfs(int index,int num0,int num1,bool flag,bool limit)//flag=1代表前导0,limit代表数位上界变量
{
    if(index==0)
        return num0>=num1;
        //return 1;
    if(!limit&&dp[index][num0][num1]!=-1)
        return dp[index][num0][num1];
    int up=limit?a[index]:1;
    ll ans=0;
    for(int i=0;i<=up;i++)
    {
        if(flag)
        {
            if(i==0)//前导是0且当前位也是0,所以num0不加1
            {
                ans+=dfs(index-1,num0,num1,flag&&i==0,limit&&i==a[index]);//后两个参模板不变
            }
            else
            {
                ans+=dfs(index-1,num0,num1+1,flag&&i==0,limit&&i==a[index]);
            }
        }
        else
        {
            if(i==0)
            {
                ans+=dfs(index-1,num0+1,num1,flag&&i==0,limit&&i==a[index]);
            }
            else
            {
                ans+=dfs(index-1,num0,num1+1,flag&&i==0,limit&&i==a[index]);
            }
        }
    }
    if(!limit)
        dp[index][num0][num1]=ans;
    return ans;
}/*
int dfs(int pos,int num0,int num1,int limit,int fzore)
{
    if(!pos)return num0>=num1;
    if(!limit&&~dp[pos][num0][num1])return dp[pos][num0][num1];
    int len=limit?a[pos]:1;
    int ans=0;
    for(int i=0;i<=len;i++)//fzore==1是代表有前导0
    {

       if(!fzore)//非前导0,即前面已有1
        {
            if(i)ans+=dfs(pos-1,num0,num1+1,limit&&i==len,fzore&&!i);
            else ans+=dfs(pos-1,num0+1,num1,limit&&i==len,fzore&&!i);
        }
        else//前导0,前面没有1
        {
            if(i)ans+=dfs(pos-1,num0,num1+1,limit&&i==len,fzore&&!i);
            else ans+=dfs(pos-1,num0,num1,limit&&i==len,fzore&&!i);
        }
    }
    if(!limit)dp[pos][num0][num1]=ans;
    return ans;
}*/
ll solve(ll x)
{
   ll pos=0;
   while(x)
   {
       a[++pos]=x%2;
       x/=2;
       //x=x>>1;
       //cout<<x<<endl;
   }
   return dfs(pos,0,0,1,1);

}
int main()
{
    ll n,m;
    scanf("%lld%lld",&n,&m);
    memset(dp,-1,sizeof(dp));
    printf("%lld\n",solve(m)-solve(n-1));
    return 0;
}

多维dp

P1541 乌龟棋

https://www.luogu.org/problemnew/show/P1541

开的主要变量:

1.f[a][b][c][d]:表示你出了a张爬行牌1,b张爬行牌2,c张爬行牌3,d张爬行牌4时的得分

2.g[x]:表示牌x一共有多少张

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[400],b[200];//a为格子分数,b为爬行卡片
int g[5];
//4种爬行卡片,每种卡片的张数不会超过40
int f[43][43][43][43];
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%d",&b[i]);
        g[b[i]]++;//记录b[i]卡片的总数目
    }
    f[0][0][0][0]=a[1];
    for(int i=0;i<=g[1];i++)
    {
        for(int j=0;j<=g[2];j++)
        {
            for(int k=0;k<=g[3];k++)
            {
                for(int t=0;t<=g[4];t++)
                {
                    int pos=1+i+j*2+k*3+t*4;
                    if(i)
                        f[i][j][k][t]=max(f[i][j][k][t],f[i-1][j][k][t]+a[pos]);
                    if(j)
                        f[i][j][k][t]=max(f[i][j][k][t],f[i][j-1][k][t]+a[pos]);
                    if(k)
                        f[i][j][k][t]=max(f[i][j][k][t],f[i][j][k-1][t]+a[pos]);
                    if(t)
                        f[i][j][k][t]=max(f[i][j][k][t],f[i][j][k][t-1]+a[pos]);
                }
            }
        }
    }
    cout<<f[g[1]][g[2]][g[3]][g[4]]<<endl;
    return 0;
}

p1005矩阵取数游戏(动态规划+高精度)

https://www.luogu.org/problem/P1005

首先讲一下

Java总结:直接ans.add(maxx),是没用的,并不会加到ans上面去,所以ans=ans.add(maxx)。另外BigInteger没有大于小于等比较符号,用compareTO,如果小于就是小于0,大于就是大于0。

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Scanner;

public class Main{
	static int n,m;
	static BigInteger b[]=new BigInteger[85];
	public static void main(String[] args) {
		Scanner in=new Scanner(System.in);
		n=in.nextInt();
		m=in.nextInt();
		b[0]=BigInteger.valueOf(1);
		for(int i=1;i<=m;i++)
		{
			b[i]=b[i-1].multiply(BigInteger.valueOf(2));
		}
		BigInteger ans=BigInteger.ZERO;
		while((n)!=0)
		{
			BigInteger f[][]=new BigInteger[85][85];
			for(int i=0;i<f.length;i++) {
				for(int j=0;j<f[i].length;j++)
					f[i][j]=BigInteger.ZERO;
			}
			int a[]=new int[85];//每一行
			for(int i=1;i<=m;i++)
			{
				a[i]=in.nextInt();
			}
			for(int i=1;i<=m;i++)
			{
				for(int j=m;j>=i;j--)
				{
					if(f[i][j].compareTo(f[i-1][j].add(b[m-j+i-1].multiply(BigInteger.valueOf(a[i-1]))))<0)
					{
						f[i][j]=f[i-1][j].add(b[m-j+i-1].multiply(BigInteger.valueOf(a[i-1])));
					}
					if(f[i][j].compareTo(f[i][j+1].add(b[m-j+i-1].multiply(BigInteger.valueOf(a[j+1]))))<0)
					{
						f[i][j]=f[i][j+1].add(b[m-j+i-1].multiply(BigInteger.valueOf(a[j+1])));
					}
				}
			}
			BigInteger maxx=BigInteger.ZERO;
			//m是列,而且之前算的f[i][i],是最终结果,i可以取1~m,现在计算那一中最大,并且最有一次还没有加上
			for(int i=1;i<=m;i++)
			{
				if(maxx.compareTo(f[i][i].add(b[m].multiply(BigInteger.valueOf(a[i]))))<0) 
				{
					maxx=f[i][i].add(b[m].multiply(BigInteger.valueOf(a[i])));
					
				}
			}
			//System.out.println(n+"maxx);
			ans=ans.add(maxx);
			//System.out.println(ans);
			n--;
			
		}
		System.out.println(ans);
		
		
		
	}
}

牛客多校第一场E .ABBA  

https://ac.nowcoder.com/acm/contest/881/E 

题意:问你长度为2 * (n+m)的字符串由(n+m)个A和B组成,要求有n个AB子序列和m个BA子序列,这样的串有几个

注意:子序列subsequence是可以不连续的,而子串substring是连续的。这个题的关键就在于想到

因为子序列n个AB和m个BA,那么显然有前n个A必为AB的A,前m个B必为BA的B。
因为如果我前n个A中有一个是BA的A,那我是不是可以从更后面随便找一个A给这个B用,而这两种情况实则是一样的,那么显然我们可以考虑为前n个A必为AB的A

#include <iostream>
#include<bits/stdc++.h>
using namespace std;
//假设有一个合法串,因为子序列n个AB和m个BA,那么显然有前n个A必为AB的A,前m个B必为BA的B。
//因为如果我前n个A中有一个是BA的A,那我是不是可以从更后面随便找一个A给这个B用,那么显然前n个A必为AB的A
#define ll long long
const ll mod=1e9+7;
ll dp[2005][2005];//dp[i][j]代表前缀有i个A,j个B
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        for(int i=0;i<=n+m;i++)
        {
            for(int j=0;j<=n+m;j++)
                dp[i][j]=0;
        }
        dp[0][0]=1;
        for(int i=0;i<=n+m;i++)
        {
            for(int j=0;j<=n+m;j++)
            {
                if(i-j<n)//因为前n个为A ,所以如果A的个数还没有大于B n个,则应该加A
                    dp[i+1][j]+=dp[i][j];
                if(j-i<m)//同理
                    dp[i][j+1]+=dp[i][j];
                dp[i+1][j]%=mod;
                dp[i][j+1]%=mod;
            }
        }
        printf("%lld\n",dp[n+m][n+m]);
    }
    return 0;
}

牛客多校第5场 G subsequence 1(动态规划+组合数)

题目大意:
给你两个数字字符串是s,t,求字符串s的子序列比字符串t大的个数。

题目思路:
首先,题目我们可以分成长度与t相等的字符串数量和长度大于t的字符串数量。
计算长度与t相等的字符串数量可以用dp解决。

设dp[i][j]表示 s的前i个,匹配 t 的前 j 个的种类数

if(s[i] == t[j]) dp[i][j] = dp[i -1][j] + dp[i - 1][j - 1]; //等于的话就等于加上与不加上相加

if(s[i] < t[j]) dp[i][j] = dp[i - 1][j]; //小于的话就不用算进去了,长度相等时一定会小于,同时可以排除前导零

if(s[i] > s[j]) ans+=dp[i - 1][j - 1] * C[len1 - i][len2 - j].//前面匹配j-1的种类数*后面随便选len2-j个,大于后长度相等时一定是大于

计算长度大于t的字符串数量可以直接用组合数解决。
直接枚举第一个(排除前导零)因为长度比t大的,一定会大于t,所以ans+=C[len1-i][j]);直接用组合数(i是枚举第一个数,j是枚举后面取几个)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 3e3+10;
const ll mod = 998244353;
ll dp[N][N],ans,C[N][N];
char s[N],t[N];
int n,m;
//dp[i][j]:从s串的前i个为数里选出j位数(长度为j的子序列)
//与t的前j位数相等的子序列数
int main(void)
{
    int T;
    scanf("%d",&T);
    C[0][0]=1;
    for(int i=1;i<=N-10;i++)
    {
        C[i][0]=1;
        for(int j=1;j<=i;j++)
            C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
    }
    while(T--)
    {
        scanf("%d%d%s%s",&n,&m,s+1,t+1);
        ans=0;

        //选出子序列长度等于t的长度,要保证前面某几位相同
        //中间至少一位大于t

        //初始化,选出0位数的子序列数肯定为1
        //即一个也不选
        for(int i=0;i<=n;i++)
            dp[i][0]=1;

        for(int i=1;i<=n;i++)
            for(int j=1;j<=min(i,m);j++)
            {
                //首先不管s[i]与t[j]有怎样的大小关系,总会有dp[i-1][j]个子序列满足条件
                //也就是不选s串中的第i位数
                dp[i][j]=dp[i-1][j];
                //只有s[i]==t[j]时,才能选s[i],那要保证从s串的前i-1为中
                //选出来的j-1位数要相等
                if(s[i]==t[j]) dp[i][j]=(dp[i][j]+dp[i-1][j-1])%mod;
                //既然从s串中选出的子序列组成的数第j位已经大于t的第j位了,
                //那么后面无论怎么选都大于t表示的数
                if(s[i]>t[j])
                    ans=(ans+dp[i-1][j-1]*C[n-i][m-j]%mod)%mod;
            }
        //选出子序列的长度大于t的长度的情况
        //直接用组合数学公式算
        for(int i=1;i<=n;i++)
        {
            if(s[i]=='0') continue;
            for(int j=m;j<=n-i;j++)//i位充当第一个数字,所以在后n-i位中再选j位
                ans=(ans+C[n-i][j])%mod;
        }
        printf("%lld\n",ans);

    }

    return 0;
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值