DP训练——树状数组优化DP

树状数组DP

BZOJ1264
题意
给出两个长度均为5*n(n<=20000)的数字序列,求LCS
数据保证每个序列中,1nn个数字每个必然出现5次。
题解
题目说了一个数会出现5次,那么我们可以预处理得到:第一个序列a[]每个数字分别在哪些位置。
因为求LCS的状态转移方程中当 s1[i-1] == s2[j-1]时,dp[i][j] = dp[i-1][j-1] + 1
即只有当两个点相同时,值才会+1
于是,我们可以对第二个序列b[]遍历一遍,对于b[i]我们可以找到它在a[]上的5个位置,这5个位置的dp[pos]都可以被更新。
更新时用树状数组维护dp数组f即可。
代码如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#include<ctime>
#include<string>
#include<bitset>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=20000*5+5;
const int INF=0x3f3f3f3f;
int n;
int a[maxn],b[maxn];
int c[maxn];
vector<int>p[maxn/5];
int f[maxn]; //f[i]表示以a[i]结尾的与b序列的最长LCS长度
int ans=0;
void add(int x,int v){
    for(int i=x;i<=n;i+=i&-i) c[i]=max(c[i],v);
}
int query(int x){
    int res=0;
    for(int i=x;i>=1;i-=i&-i) res=max(res,c[i]);
    return res;
}
void dp(){
    for(int i=1;i<=n;i++){
        for(int j=p[b[i]].size()-1;j>=0;j--){
            int pos=p[b[i]][j];
            f[pos]=query(pos-1)+1;
//            D(pos);D(f[pos]);E;
            add(pos,f[pos]);
        }
    }
    for(int i=1;i<=n;i++) ans=max(ans,f[i]);
}
int main() {
    ios::sync_with_stdio(false);
    //freopen("BZOJ1264.in","r",stdin);
    scanf("%d",&n);
    n*=5;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        p[a[i]].push_back(i); //记录每个数字的位置
    }
    for(int i=1;i<=n;i++) scanf("%d",&b[i]);
    dp();
    printf("%d",ans);
    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif

BZOJ3594
题意
n(n<=10000)个数字排成一行,最多进行k(k<=500)次操作。
每次可以将一个区间内所有数字+1,或者去除任意个数字。
为使得最后的序列单调不下降,求最后剩余的数字个数的最大值。
题解
状态定义:f[i,j]表示以i结尾,被j个拔高区间覆盖过的最长单调不下降子序列的长度
目标:max(f[i,j])
边界:f[0,0]=0
转移方程:f[i,j]=max(f[k,l])+1,k<i,l<=j,h[k]+l<=h[i]+j
时间复杂度O(n^{2}k^{2})
考虑优化时间复杂度。
正常的二维树状数组只可以维护k<i,l<=j,h[k]+l<=h[i]+j中的两个条件,又注意到第一维可以用类似01背包的思想压缩。
因此将状态第一维压缩,将h[k]+l<=h[i]+j这个条件转化到状态定义中,再将第三维的枚举改为逆序循环(避免第一维均为i的状态间相互转移),即可用二维树状数组维护f数组的最值。
状态定义:f[j,k]表示以i结尾,高度(拔高后的高度)小于等于j(j=h[i]+k),被k个拔高区间覆盖过的最长单调不下降子序列的长度
目标:max(f[j,k])
边界:f[,0]=0
转移方程:f[j,k]=max(f[y,z])+1,j<=y,z<=k
PS:
拔高次数范围为[0,k],树状数组不能处理0这个下标,所以这里将范围转化为[1,k+1]
因为f数组为中间量,所以并没有在代码中显式定义。
题解链接 https://www.luogu.com.cn/problemnew/solution/P3287
代码如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#include<ctime>
#include<string>
#include<bitset>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=10000+5;
const int maxa=5000+5;
const int maxk=500+5;
const int INF=0x3f3f3f3f;
int n,k;
int a[maxn];
int c[maxa+maxk][maxk];
int mx1=-INF,mx2;
int ans=-INF;
void add(int x,int y,int v){
    for(int i=x;i<=mx1;i+=i&-i) for(int j=y;j<=mx2;j+=j&-j) c[i][j]=max(c[i][j],v);
}
int ask(int x,int y){
    int res=-INF;
    for(int i=x;i>=1;i-=i&-i) for(int j=y;j>=1;j-=j&-j) res=max(res,c[i][j]);
    return res;
}
void dp(){
    /*
    f[i,j,k]表示当前这个点i是(从左往右枚举点),
    以一个高度(拔高后的高度)小于等于j(j=h[i]+k),
    被小于等于k个拔高区间覆盖过的点为结尾的最长不下降序列的长度。
    利用类似于01背包的特性,可以去掉状态的第一维,但k的循环要变成逆序。(状态中的j可以通过i和k算出)
    c[i,j]表示以高度小于等于i,被拔高区间的覆盖次数小于等于j的点为结尾的所有不下降序列长度的最大值
    */
    for(int i=1;i<=n;i++){
        for(int j=k;j>=0;j--){
            int temp=ask(a[i]+j,j+1)+1; //j+1的原因是树状数组不能处理0这个下标,所以这一维上的下标需要向右平移一个单位
            ans=max(ans,temp);
            add(a[i]+j,j+1,temp);
        }
    }
}
int main() {
//  ios::sync_with_stdio(false);
    //freopen("BZOJ3594.in","r",stdin);
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        mx1=max(mx1,a[i]);
    } 
    mx1+=k; //最大玉米高度为max{a[i]}+k
    mx2=k+1; //拔高次数范围为[0,k],树状数组不能处理0这个下标,所以这里将范围转化为[1,k+1]
    dp();
    printf("%d",ans);
    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif

BZOJ2131
题意
n(n<=1e5)个馅饼,第i个馅饼分值为v[i]t[i](t[i]<=1e8)时刻,落在p[i]位置,p[i]处于[1,W](W<=1e8)坐标范围内。
玩家初始时刻可处于任意位置,每个时刻可以向左右移动不超过两格,求接到的馅饼分值和的最大值。
题解
首先考虑朴素dp,将馅饼按照落地时间排序。
状态定义:f[i]表示接住第i个馅饼的最大分值
目标:max_{1<=i<=n}f[i]
边界:f[0]=0
转移方程:f[i]=max_{j<i}(f[j])+v[i],|p[i]-p[j]|<=2*(t[i]-t[j])
时间复杂度O(n^2)
考虑优化时间复杂度。
注意到转移时需要维护满足|p[i]-p[j]|<=2*(t[i]-t[j])的最大f[j],故尝试拆解该条件。
去绝对之后,该条件转换为
-2*t[i]+2*t[j]<=p[i]-p[j]
p[i]-p[j]<=2*t[i]-2*t[j]
分离参数得
2*t[j]+p[j]<=p[i]+2*t[i]
2*t[j]-p[j]<=2*t[i]-p[i]
2*t[i]+p[i]=a[i],2*t[i]-p[i]=b[i]
a[j]<=a[i],b[j]<=b[i]
即将条件转化为维护二维前驱最大值,使用二维树状数组维护即可。
PS:为了解决二维树状数组空间过大的问题,可对b数组进行排序后,离散化a数组。
题解链接 https://blog.csdn.net/jaihk662/article/details/78145476
代码如下

/*

*/
#define method_1
#ifdef method_1
/*

*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#include<ctime>
#include<string>
#include<bitset>
#define D(x) cout<<#x<<" = "<<x<<"  "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=1e5+5;
const int INF=0x3f3f3f3f;
int W,n;
struct node{
    int x,y,v;
    bool operator<(const node& h)const{return y<h.y||(y==h.y&&x<h.x);}
}pie[maxn];
int f[maxn];
int c[maxn];
int a[maxn],cnt=0;
void add(int x,int v){
    for(int i=x;i<=cnt;i+=i&-i) c[i]=max(c[i],v);
}
int ask(int x){
    int res=-INF;
    for(int i=x;i>=1;i-=i&-i) res=max(res,c[i]);
    return res;
}
void pre(){
    sort(pie+1,pie+n+1);
    sort(a+1,a+cnt+1);
    cnt=unique(a+1,a+cnt+1)-a-1;
}
void dp(){
    for(int i=1;i<=n;i++){ //循环顺序保证了y递增
        int pos=lower_bound(a+1,a+cnt+1,pie[i].x)-a; //二分查找后 得到符合条件的x下标
        f[pos]=ask(pos)+pie[i].v;
        add(pos,f[pos]);
    }
}
int main() {
    ios::sync_with_stdio(false);
    //freopen("BZOJ2131.in","r",stdin);
    scanf("%d%d",&W,&n);
    int t,p;
    for(int i=1;i<=n;i++){
        scanf("%d%d%d",&t,&p,&pie[i].v);
        pie[i].x=2*t+p;
        pie[i].y=2*t-p;
        a[++cnt]=pie[i].x;
    }
    pre();
    dp();
    printf("%d",ask(cnt)); //注意这里不是f[cnt],因为此时的f[cnt]里存储值时没有加入最后一块馅饼的状态
    return 0;
}
#endif
#ifdef method_2
/*

*/

#endif
#ifdef method_3
/*

*/

#endif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335