2022牛客寒假算法基础集训营3

本文探讨了动态规划在背包问题中的两种解法,包括二维数组和一维滚动数组的实现,强调了逆向思维在解决特殊背包问题中的作用。此外,还介绍了如何通过模拟和标记来处理多元组种类和个数的问题,以及在字符串匹配和大数计算中的应用。文章通过具体的代码示例展示了这些方法的实现细节和思路。
摘要由CSDN通过智能技术生成

B-01背包求方案数

链接

写法一:二维

和第一场的A题同种做法。
注意:

  1. d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i个西瓜,能够拼凑出西瓜重量为 j j j的最大方案数,注意两维数组根据题目数据范围开得大一点!!(一开始样例都过但是答案没过就卡在这了)
  2. d p [ 0 ] [ 0 ] = 1 dp[0][0]=1 dp[0][0]=1
  3. 三种情况得到三种递推式子
    (1) 第i个西瓜不选, d p [ i ] [ j ] + = d p [ i − 1 ] [ j ] dp[i][j]+=dp[i-1][j] dp[i][j]+=dp[i1][j]
    (2)选定第 i i i个西瓜,影响的是 j + w [ i ] j+w[i] j+w[i] j + w [ i ] / 2 j+w[i]/2 j+w[i]/2
    第一种: d p [ i ] [ j + w [ i ] ] + = d p [ i − 1 ] [ j ] dp[i][j+w[i]] +=dp[i-1][j] dp[i][j+w[i]]+=dp[i1][j]
    第二种: d p [ i ] [ j + w [ i ] / 2 ] + = d p [ i − 1 ] [ j ] dp[i][j+w[i]/2] +=dp[i-1][j] dp[i][j+w[i]/2]+=dp[i1][j]
#include<iostream>
using namespace std;
const int N=1100,mod=1e9+7;
long long dp[N][5000],w[N];

int main()
{
    int n,m;
    cin>>n>>m;
    
    for(int i=1;i<=n;i++) cin>>w[i];
    
    dp[0][0]=1;
    for(int i=1;i<=n;i++)   
        for(int j=0;j<=m;j++)
        {
            dp[i][j]=(dp[i][j]+dp[i-1][j])%mod;
            dp[i][j+w[i]]=(dp[i][j+w[i]]+dp[i-1][j])%mod;
            dp[i][j+w[i]/2]=(dp[i][j+w[i]/2]+dp[i-1][j])%mod;
        }
    
    for(int i=1;i<=m;i++) cout<<dp[n][i]%mod<<" ";
    return 0;
}

写法二: 一维滚动数组

d p [ i ] [ j ] = ( d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − w [ i ] ] + d p [ i − 1 ] [ j − w [ i ] / 2 ] ) dp[i][j]=(dp[i-1][j]+dp[i-1][j-w[i]]+dp[i-1][j-w[i]/2]) dp[i][j]=(dp[i1][j]+dp[i1][jw[i]]+dp[i1][jw[i]/2])% m o d mod mod
可以转化为滚动数组, f o r for for循环第二层 j j j要逆序写,
d p [ j ] = ( d p [ j ] + d p [ j − w [ i ] ] + d p [ j − w [ i ] / 2 ] ) dp[j]=(dp[j]+dp[j-w[i]]+dp[j-w[i]/2]) dp[j]=(dp[j]+dp[jw[i]]+dp[jw[i]/2])% m o d mod mod,
i f if if判读:
i f ( j ≥ w [ i ] ) , d p [ j ] = ( d p [ j ] + d p [ j − w [ i ] ] + d p [ j − w [ i ] / 2 ] ) if(j≥w[i]),dp[j]=(dp[j]+dp[j-w[i]]+dp[j-w[i]/2]) if(jw[i])dp[j]=(dp[j]+dp[jw[i]]+dp[jw[i]/2])% m o d mod mod
i f ( j ≥ w [ i ] / 2 ) , d p [ j ] = ( d p [ j ] + d p [ j − w [ i ] ] / 2 ) if(j≥w[i]/2),dp[j]=(dp[j]+dp[j-w[i]]/2) if(jw[i]/2)dp[j]=(dp[j]+dp[jw[i]]/2)% m o d mod mod

#include<iostream>
using namespace std;
const int N=1100,mod=1e9+7;
long long dp[N][5000],w[N];

int main()
{
    int n,m;
    cin>>n>>m;
    
    for(int i=1;i<=n;i++) cin>>w[i];
    
    dp[0][0]=1;
    for(int i=1;i<=n;i++)   
        for(int j=0;j<=m;j++)
        {
            dp[i][j]=(dp[i][j]+dp[i-1][j])%mod;
            dp[i][j+w[i]]=(dp[i][j+w[i]]+dp[i-1][j])%mod;
            dp[i][j+w[i]/2]=(dp[i][j+w[i]/2]+dp[i-1][j])%mod;
        }
    
    for(int i=1;i<=m;i++) cout<<dp[n][i]%mod<<" ";
    return 0;
}

C—背包/(逆向思维)已知方案数还原数据

  1. 如果发现 d p [ 1 ] = 1 dp[1]=1 dp[1]=1,因为西瓜的质量只能是偶数,那么重量 1 1 1一定是由重量为 2 2 2的西瓜切一半得到的。所以一定存在重量为 2 2 2的西瓜,我们把重量为 2 2 2的西瓜产生的所有方案数去除,如果去除后发现 d p [ 2 ] ≠ 0 dp[2]≠0 dp[2]=0,说明这个重量 2 2 2一定是由重量为4的西瓜切一半得到的,类推。
  2. 为什么当发现 d p [ i ] ≠ 0 dp[i]≠0 dp[i]=0时认为 i i i一定是由重量为 i ∗ 2 i*2 i2的西瓜切一半得到的,而不是重量为 i i i的整个西瓜得到的?
    因为我们从前往后处理,如果存在当前重量 i i i,因为可以买半个,那么一定会影响到前面 i / 2 i/2 i/2的值导致错误,所以每个重量都认为从半个西瓜得到的,从而向后递推。
  3. 重量 j j j的方案数方程为: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − w [ i ] ] + d p [ i − 1 ] [ j − w [ i ] / 2 ] 。 dp[i][j]=dp[i-1][j]+dp[i-1][j-w[i]]+dp[i-1][j-w[i]/2]。 dp[i][j]=dp[i1][j]+dp[i1][jw[i]]+dp[i1][jw[i]/2]即: d p [ j ] + = d p [ j − w [ i ] ] + d p [ j − w [ i ] / 2 ] dp[j]+=dp[j-w[i]]+dp[j-w[i]/2] dp[j]+=dp[jw[i]]+dp[jw[i]/2]
    所以如果存在重量 j j j,消除j产生的方案数,即 d p [ j + w [ i ] ] − = d p [ j ] , d p [ i + w [ i ] / 2 ] − = d p [ j ] dp[j+w[i]]-=dp[j],dp[i+w[i]/2]-=dp[j] dp[j+w[i]]=dp[j],dp[i+w[i]/2]=dp[j]
    本题中,如果 d p [ i ] ≠ 0 dp[i]≠0 dp[i]=0,则一定存在 i ∗ 2 i*2 i2,则 d p [ j + 2 ∗ i ] − = d p [ j ] , d p [ j + i ] − = d p [ j ] dp[j+2*i]-=dp[j],dp[j+i]-=dp[j] dp[j+2i]=dp[j],dp[j+i]=dp[j]
#include<iostream>
#include<vector>
using namespace std;
const int N=10010,mod=1e9+7;
int dp[N];
vector<int>ans;
int main()
{
    int m;
    cin>>m;
    for(int i=1;i<=m;i++) cin>>dp[i];
    
    dp[0]=1;
    for(int i=1;i<=m;i++)
        while(dp[i])
        {
            ans.push_back(i*2);
            for(int j=i;j<=m;j++)
            {
                if(j>=i) dp[j]-=dp[j-i];
                if(j>=2*i) dp[j]-=dp[j-2*i];
                while(dp[j]<0) dp[j]+=mod;
            }
        }

    cout<<ans.size()<<endl;
    for(int i=0;i<ans.size();i++)
        cout<<ans[i]<<" ";
    return 0;
}

L-模拟/标记多元组的种类及个数

  1. 不同字符串代表着不同的种类,将字符串映射为整数标记—— m a p < s t r i n g , i n t > m p 1 map<string,int>mp1 map<string,int>mp1
  2. 多元组是一个整数集合,相当于 v e c t o r < i n t > vector<int> vector<int>, m a p < v e c t o r < i n t > , i n t > m p 2 map<vector<int>,int>mp2 map<vector<int>,int>mp2标记不同多元组的个数, m p 2. s i z e ( ) mp2.size() mp2.size()就是多元组的种类。
#include<vector>
#include<map>
#include<iostream>
using namespace std;
const int N=1100;
int a[N][N];
map<string,int>mp1;
map<vector<int>,int>mp2;
vector<int>p;

int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        string s;
        cin>>s;
        mp1[s]=i;
    }
    
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++)
            cin>>a[i][j];
    
    getchar();
    string str;
    getline(cin,str);
    
    int j=36;
    for(int i=37;i<str.size();i++)
        if(str[i]==','||str[i]==';')
        {
            string ss=str.substr(j,i-j);
            p.push_back(mp1[ss]);
            j=i+1;
        }
    
    for(int i=0;i<n;i++)
    {
        vector<int>k;
        for(auto j:p)
            k.push_back(a[i][j]);
        mp2[k]++;
    }
    
    cout<<mp2.size()<<endl;
    
    for(auto i:mp2)
        cout<<i.second<<" ";
    
    return 0;
}

I-智乃的密码

方法1:尺取法/双指针

关键是要找到单调性,当右端点符合条件时,后面所有的点都可以符合条件了,并且此后左端点向后移动时,右端点也只会在上次移动的基础上继续向右移动,不会向左退。

  1. 先将串中每一位字符分类
  2. 每次固定左端点 i i i,找到第一个符合条件的右端点 j j j,就可以开始计算长度了, ( j , n ) (j,n) (j,n)区间和 ( i + l , i + r ) (i+l,i+r) (i+l,i+r)区间取交集。
#include<iostream>
using namespace std;
const int N=1e5+100;
int a[N];
long long b[5];    //存储4类字符各有多少个
int main()
{
    int n,l,r;
    cin>>n>>l>>r;
    
    for(int i=0;i<n;i++)
    {
        char c;
        cin>>c;
        if(c>='A'&&c<='Z') a[i]=1;
        else if(c>='a'&&c<='z') a[i]=2;
        else if(c>='0'&&c<='9') a[i]=3;
        else a[i]=4;
    }
    
    int j=0;
    long long ans=0;
    for(int i=0;i<n;i++)	//固定左端点
    {
        while(j<=n&&((b[1]>0)+(b[2]>0)+(b[3]>0)+(b[4]>0))<3)	//右端点右移
        {
            b[a[j]]++;
            j++;
        }

        int x=max(j,i+l);
        int y=min(n,i+r);
        if(x<=y) ans+=y-x+1;
        b[a[i]]--;
    }
    
    cout<<ans<<endl;
    
    return 0;
}

方法2:前缀和+二分

  1. 因为一共有四种类型的字符,建立四个前缀和数组分别存储四种类型字符的数目
  2. 固定左端点 i i i,在 ( i + l , i + r ) (i+l,i+r) (i+l,i+r)区间内找到符合条件的最右端点,得到答案区间
#include<iostream>
using namespace std;
typedef long long LL;
const int N=1e5+100;
int a[N],b[N],c[N],d[N];
char s;

bool check(int l,int r)
{
    int x=a[r]-a[l-1],y=b[r]-b[l-1],z=c[r]-c[l-1],p=d[r]-d[l-1];
    if(((x>=1)+(y>=1)+(z>=1)+(p>=1))>=3) return 1;
    return 0;
}
int main()
{
    int n,l,r;
    cin>>n>>l>>r;
    
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        if(s>='A'&&s<='Z') a[i]++;
        else if(s>='a'&&s<='z') b[i]++;
        else if(s>='0'&&s<='9') c[i]++;
        else d[i]++;
        
        a[i]+=a[i-1],b[i]+=b[i-1],c[i]+=c[i-1],d[i]+=d[i-1];
    }
    
    LL ans=0;
    
    for(int i=1;i+l-1<=n;i++)
    {
        int ll=min(i+l-1,n),rr=min(i+r-1,n);    //区间(i+l-1,i+r-1)
        int y=rr;    //答案区间右边界
        while(ll<rr)
        {
            int mid=(ll+rr)/2;
            if(check(i,mid)) rr=mid;
            else ll=mid+1;
        }
        if(check(i,ll))	//注意二分之后的特判
            ans+=y-ll+1;
    }
    
    cout<<ans<<endl;   
    return 0;
}

E-模拟

注意大数取模只能从最高位到最低位模拟,边乘边取模。不能从最低位从右向左

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int mod=1e9+7,N=1e5+100;
typedef long long LL;
int color[N],n,m,k;
char s[N];
bool cmp(char a,char b)
{
    return a>b;
}
void put()
{
	LL ans=0;
    for(int i=0;i<n;i++)
    {
        ans=(ans*10+s[i]-'0')%mod;		//大数取模
    }
	cout<<ans%mod<<endl;
}
void  work()
{
    for(int i=0;i<n-1;i++)
    {
        if(color[i]==color[i+1])
        {
            int j=i+1;
            while(color[j]==color[i]&&j<n) j++;
            sort(s+i,s+j,cmp);
            i=j-1;
        }
    }
    // cout<<s<<endl;
}
int main()
{
    cin>>n>>m>>k;
    cin>>s;
    
    for(int i=0;i<n;i++) cin>>color[i];
    
    work();
    put();
    // cout<<s<<endl;
    
    while(k--)
    {
        int a,b;
        cin>>a>>b;
        for(int i=0;i<n;i++)
        if(color[i]==a) 
            color[i]=b;
        
        work();
        put();
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值