数据结构优化dp题集


hdu5542 The Battle of Chibi

题意:

给长度为n的数组a,和一个整数m
要求计算数组中长度为m的子序列数量,满足子序列严格递增
答案对1e9+7取模
数据范围:n,m<=1e3

解法:
因为n和m只有1e3,:
令d[i][j]表示弟以i结尾长度为j的上升子序列数量
容易想到转移方程为:d[i][j]=sigma(d[k][j-1]),其中1<=k<i,且a[k]<a[i]
枚举i,j是O(n^2),加上k则为O(n^3),需要优化
考虑到第三维是累加长度为j-1的所有d[k],a[k]<a[i]
可以用二维树状数组c[x][j]存储以值x为结尾的长度为j的上升子序列数量
这样第三维就能降到log(n),总复杂度O(n^2*log(n))
code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=1e3+5;
const int mod=1e9+7;
int d[maxm][maxm];
int c[maxm][maxm];
int xx[maxm];
int a[maxm];
int n,m;
int lowbit(int i){
    return i&-i;
}
void add(int x,int len,int t){
    while(x<maxm){
        c[x][len]+=t;
        c[x][len]%=mod;
        x+=lowbit(x);
    }
}
int ask(int x,int len){
    int ans=0;
    while(x){
        ans+=c[x][len];
        ans%=mod;
        x-=lowbit(x);
    }
    return ans;
}
signed main(){
    int T;
    cin>>T;
    int cas=1;
    while(T--){
        memset(c,0,sizeof c);
        cin>>n>>m;
        for(int i=1;i<=n;i++){
            cin>>a[i];
            xx[i]=a[i];
        }
        sort(xx+1,xx+1+n);
        int num=unique(xx+1,xx+1+n)-xx-1;
        for(int i=1;i<=n;i++){
            a[i]=lower_bound(xx+1,xx+1+num,a[i])-xx+3;
        }
        for(int i=1;i<=n;i++){
            d[i][1]=1;
            add(a[i],1,1);
            for(int j=2;j<=m&&j<=i;j++){
                d[i][j]=ask(a[i]-1,j-1);
                add(a[i],j,d[i][j]);
            }
        }
        int ans=0;
        for(int i=1;i<=n;i++){
            ans+=d[i][m];
            ans%=mod;
        }
        printf("Case #%d: ",cas++);
        cout<<ans<<endl;
    }
    return 0;
}

CodeForces833 B. The Bakery

题意:

给长度为n的数组a,和一个整数k
要求把数组分成连续的k段,每段的权值是该段中不同数的个数,
输出最大权值和。
数据范围:n<=35000,k<=min(n,50),1<=a(i)<=n

完整题意:
n个蛋糕k个盒子,要求把蛋糕分成来k段,每段连续,分别用盒子装,
每个盒子的价值是盒子内不同蛋糕的数量,要求计算最大价值

解法:
d[i][j]表示前i个盒子装下前j蛋糕的最大价值
col[i][j]表示区间[i,j]中不同数的个数
显然d[1][j]=col[1][j]

考虑在原来的基础上再切一刀,可推出转移方程为:
d[i][j]=max{d[i-1][k]+col[k+1][i]},其中k<j

因为要枚举i,j,k,因此复杂度是O(n^2*k)的
而n最大35000,这个复杂度显然是不满足要求的,想办法优化掉一个n

因为d[i][j]=max{d[i-1][k]+col[k+1][i]},max操作可以想到线段树
但是要先将d[i-1][k]+col[k+1][i]全部计算出来

建立一颗线段树,第k个位置为d[i-1][k-1]

考虑每个数有价值的区间:
每个数有价值的范围由前一个与他相同数的位置决定,例如:
a[3]=5,a[5]=5,则a[5]有价值的区间为[4,5],
通过记录前一个数的位置可以O(n)把每个数有价值的区间求出来

假如一个数j的有价值区间为[a,b],则对线段树的[a,b]区间加1贡献
这样之后线段树中的每个位置就是d[i-1][k]+col[k+1][i][1,j]中的max来更新d[i][j]

---
总结:
dp转移的过程中,一些数据需要直接计算
有时候可以拆成若干小数据分别计算贡献
code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxm=4e4+5;
int laz[maxm<<2],t[maxm<<2];
int d[55][maxm];
int mark[maxm];
int pre[maxm];
int a[maxm];
int n,k;
void pushup(int node){
    t[node]=max(t[node*2],t[node*2+1]);
}
void pushdown(int node){
    if(laz[node]){
        laz[node*2]+=laz[node];
        laz[node*2+1]+=laz[node];
        t[node*2]+=laz[node];
        t[node*2+1]+=laz[node];
        laz[node]=0;
    }
}
void build(int l,int r,int node,int i){
    t[node]=laz[node]=0;
    if(l==r){
        t[node]=d[i-1][l-1];
        return ;
    }
    int mid=(l+r)/2;
    build(l,mid,node*2,i);
    build(mid+1,r,node*2+1,i);
    pushup(node);
}
void update(int st,int ed,int l,int r,int node){
    if(st<=l&&ed>=r){
        laz[node]++;
        t[node]++;
        return ;
    }
    pushdown(node);
    int mid=(l+r)/2;
    if(st<=mid)update(st,ed,l,mid,node*2);
    if(ed>mid)update(st,ed,mid+1,r,node*2+1);
    pushup(node);
}
int ask(int st,int ed,int l,int r,int node){
    if(st<=l&&ed>=r){
        return t[node];
    }
    pushdown(node);
    int mid=(l+r)/2;
    int ans=0;
    if(st<=mid)ans=max(ans,ask(st,ed,l,mid,node*2));
    if(ed>mid)ans=max(ans,ask(st,ed,mid+1,r,node*2+1));
    return ans;
}
signed main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=1;i<=n;i++){
        pre[i]=mark[a[i]]+1;
        mark[a[i]]=i;
    }
    for(int i=1;i<=k;i++){
        build(1,n,1,i);
        for(int j=1;j<=n;j++){
            update(pre[j],j,1,n,1);
            d[i][j]=ask(1,j,1,n,1);
        }
    }
    cout<<d[k][n]<<endl;
    return 0;
}

UVA1401 Remember the Word

题意:

给定串S,长度不超过3e5
再给n个串t(i),每个串长度不超过100,n<=4e4
现在要将S拆分为若干t(i)的连接,问有多少种组合方法,答案对20071027取模

样例:
S串:abcd
t(i):a、b、cd、ab
有两种组合方法:a+b+cd,ab+cd

解法:
d[i]表示从位置i开始的串的后缀分解方案数那么答案为d[0]
那么显然有转移方程:d[i]=sum{d[i+len(x)]},其中x为给定的串,且x是后缀suf(i)的前缀
最多4000个串,如果暴力用后缀suf(i)去判断是否有前缀串,肯定不行
可以先对给定串建立字典树,在字典树上找匹配串,因为串的长度不超过100,每次最多搜100层

总结:多串匹配转移可以用字典树减少总比较次数
code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=4e5+5;
const int mod=20071027;
vector<int>temp;
struct Trie{
    int a[maxm*26][26],tot;
    int dep[maxm*26];//字符串长度
    void init(){
        for(int i=0;i<=tot;i++){
            for(int j=0;j<26;j++)a[i][j]=0;
            dep[i]=0;
        }
        tot=0;
    }
    void add(char* s){
        int node=0;
        int len=strlen(s);
        for(int i=0;i<len;i++){
            int v=s[i]-'a';
            if(!a[node][v])a[node][v]=++tot;
            node=a[node][v];
        }
        dep[node]=len;
    }
    void get(char *s,int len){
        int node=0;
        for(int i=0;i<len;i++){
            int v=s[i]-'a';
            if(!a[node][v])return ;
            node=a[node][v];
            if(dep[node])temp.push_back(dep[node]);
        }
    }
}T;
char s[maxm];
int d[maxm];
signed main(){
    int cas=1;
    while(scanf("%s",s)!=EOF){
        T.init();
        int n;scanf("%d",&n);
        for(int i=1;i<=n;i++){
            char t[105];scanf("%s",t);
            T.add(t);
        }
        int len=strlen(s);
        for(int i=0;i<=len;i++)d[i]=0;
        d[len]=1;
        for(int i=len-1;i>=0;i--){
            temp.clear();
            T.get(s+i,len-i);
            for(int v:temp){
                d[i]=(d[i]+d[i+v])%mod;
            }
        }
        printf("Case %d: %d\n",cas++,d[0]);
    }
    return 0;
}

hdu4719 Oh My Holy FFF

题意:

给定长度为n的序列a,和一个整数L
你需要把这个序列分成若干连续段,要求每一段的长度不超过L
假设你分了M段,设第i段的最右边一个值为b(i),还需要满足b(i)>b(i-1)
当前分段方式的权值为:
在这里插入图片描述

问最大权值是多少,如果无法按照题目要求分段,输出No solution

数据范围:n<=1e5

解法:
d[i]表示i作为结尾的的最大价值
d[i]=max{d[k]-a[k]}+d[j]*d[j],其中k取值为[j-L,j-1],a[j]>a[k]

max{d[k]-a[k]}部分可以使用线段树,
但是由于存在a[j]>a[k]的限制,直接使用线段树可能找到不满足条件的k
一种巧妙的方法是按照a的大小,从小到大进行dp,计算出d[]之后加入线段树
这样每次找到的肯定都是满足条件的k了

需要注意的点:
数组中可能存在相同的数,显然根据题目条件相同的数是不能转移的
因此排序的过程中,遇到相同的数,需要把下标大的放在前面,
让下标大的先dp

总结:
见识到了dp顺序不一定是固定的从左到右
遇到这题这种限制条件时,
可以去限制拓扑序,令不能转移过来的点后面再计算,
从而使转移满足题目条件
code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxm=1e5+5;
struct ST{
    int a[maxm<<2];
    void build(int l,int r,int node){
        a[node]=-1;
        if(l==r)return ;
        int mid=(l+r)/2;
        build(l,mid,node*2);
        build(mid+1,r,node*2+1);
    }
    void update(int x,int val,int l,int r,int node){
        if(l==r){
            a[node]=val;
            return ;
        }
        int mid=(l+r)/2;
        if(x<=mid)update(x,val,l,mid,node*2);
        else update(x,val,mid+1,r,node*2+1);
        a[node]=max(a[node*2],a[node*2+1]);
    }
    int ask(int st,int ed,int l,int r,int node){
        if(st<=l&&ed>=r)return a[node];
        int mid=(l+r)/2;
        int ans=-1;
        if(st<=mid)ans=max(ans,ask(st,ed,l,mid,node*2));
        if(ed>mid)ans=max(ans,ask(st,ed,mid+1,r,node*2+1));
        return ans;
    }
}t;
int a[maxm];
int b[maxm];
int d[maxm];
int n,L;
bool cmp(int i,int j){
    if(a[i]==a[j])return i>j;
    return a[i]<a[j];
}
signed main(){
    ios::sync_with_stdio(0);
    int T;cin>>T;
    int cas=1;
    while(T--){
        cin>>n>>L;
        for(int i=1;i<=n;i++)cin>>a[i];
        for(int i=1;i<=n;i++)b[i]=i;
        sort(b+1,b+1+n,cmp);
        t.build(0,n,1);
        for(int i=1;i<=n;i++){
            int x=b[i];
            d[x]=-1;
            if(x<=L)d[x]=a[x]*a[x];
            //
            int temp=t.ask(max(0LL,x-L),x-1,0,n,1);
            if(temp>=0)d[x]=temp+a[x]*a[x];
            //
            if(d[x]>=0)t.update(x,d[x]-a[x],0,n,1);
        }
        cout<<"Case #"<<cas++<<": ";
        if(d[n]<0)cout<<"No solution"<<endl;
        else cout<<d[n]<<endl;
    }
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值