数据结构与算法:dp优化——树状数组/线段树优化

前言

现在确实能感觉到 dp 能力有了不小的提升,感谢左神!

一、区间和的个数

太阴了这个题,思路一分钟,调试一小时......

cpp 复制代码
template<typename T>
struct BIT
{
    vector<T>tree;

    BIT(int n)
    {
        tree.resize(n);
    }

    int lowbit(int i)
    {
        return i&-i;
    }

    void add(int i,T v)
    {
        while(i<tree.size())
        {
            tree[i]+=v;
            i+=lowbit(i);
        }
    }

    T sum(int i)
    {
        T ans=0;
        while(i>0)
        {
            ans+=tree[i];
            i-=lowbit(i);
        }
        return ans;
    }

    T query(int l,int r)
    {
        return sum(r)-sum(l-1);
    }
};

typedef long long ll;

class Solution {
public:
    int countRangeSum(vector<int>& nums, int lower, int upper) {
        int n=nums.size();

        //对前缀和离散化
        vector<ll>sorted(n+1);
        for(int i=1;i<=n;i++)
        {
            sorted[i]=sorted[i-1]+nums[i-1];
        }

        sort(sorted.begin()+1,sorted.end());

        int len=1;
        for(int i=2;i<=n;i++)
        {
            if(sorted[len]!=sorted[i])
            {
                sorted[++len]=sorted[i];
            }
        }

        //找小于等于v的最右位置
        auto bs=[&](ll v)->int
        {
            int l=1;
            int r=len;
            int m;
            int ans=0;
            while(l<=r)
            {
                m=l+r>>1;
                if(sorted[m]<=v)
                {
                    ans=m;
                    l=m+1;
                }
                else
                {
                    r=m-1;
                }
            }
            return ans;
        };

        BIT<ll>tree(len+1);
        ll sum=0;
        int ans=0;
        for(int i=0;i<n;i++)
        {
            sum+=nums[i];
            if(lower<=sum&&sum<=upper)
            {
                ans++;
            }

            //[sum-upper,sum-lower]这个范围里的个数
            //小于等于sum-lower的个数 减去 小于等于sum-upper-1的个数
            int left=bs(sum-upper-1);
            int right=bs(sum-lower);
            ans+=tree.sum(right)-tree.sum(left);

            int idx=bs(sum);
            tree.add(idx,1);
        }
        return ans;
    }
};

对于区间和问题,肯定还是转化成前缀和来解决。而对于区间计数问题,肯定还是考虑固定一个端点,然后快速求合法的另一个端点的个数。所以对于当前来到的位置 i,前缀和为 sum,如果要求区间和在 [l,r] 范围内,那么就是查之前在 [sum-r,sum-l] 范围内的前缀和个数,这个可以通过二分来解决。那么范围查就可以通过树状数组解决了,只需要一开始对前缀和离散化即可。

二、平衡子序列的最大和

cpp 复制代码
typedef long long ll;

const ll INF=1e18;

template<typename T>
struct BIT
{
    vector<T>tree;

    BIT(int n,T v=0)
    {
        tree.resize(n,v);
    }

    int lowbit(int i)
    {
        return i&-i;
    }

    void update(int i,T v)
    {
        while(i<tree.size())
        {
            tree[i]=max(tree[i],v);
            i+=lowbit(i);
        }
    }

    T query(int i)
    {
        T ans=-INF;
        while(i>0)
        {
            ans=max(ans,tree[i]);
            i-=lowbit(i);
        }
        return ans;
    }
};

class Solution {
public:
    long long maxBalancedSubsequenceSum(vector<int>& nums) {
        int n=nums.size();

        //式子nums[i]-nums[j]>=i-j,可以转化为nums[i]-i>=nums[j]-j
        //所以可以只关注nums[i]-i这个值
        //对于当前值v,能产生子序列的就是前面值小于等于v的位置中的最大值
        //所以可以考虑用树状数组维护最大值

        //离散化
        vector<ll>sorted(n+1);
        for(int i=1;i<=n;i++)
        {
            sorted[i]=nums[i-1]-(i-1);
        }

        sort(sorted.begin()+1,sorted.end());

        int len=1;
        for(int i=2;i<=n;i++)
        {
            if(sorted[len]!=sorted[i])
            {
                sorted[++len]=sorted[i];
            }
        }

        auto bs=[&](ll v)->int
        {
            int l=1;
            int r=len;
            int m;
            int ans=0;
            while(l<=r)
            {
                m=(l+r)>>1;
                if(sorted[m]>=v)
                {
                    ans=m;
                    r=m-1;
                }
                else
                {
                    l=m+1;
                }
            }
            return ans;
        };

        //维护最大值的树状数组
        BIT<ll>tree(len+1,-INF);

        for(int i=0;i<n;i++)
        {
            int rank=bs(nums[i]-i);

            ll pre=tree.query(rank);

            //之前的最大值都小于0 -> 自己单独
            if(pre<0)
            {
                tree.update(rank,nums[i]);
            }
            else
            {
                tree.update(rank,pre+nums[i]);
            }
        }

        return tree.query(len);
    }
};

不难想到,对于这个式子 nums[i]-nums[j]>=i-j,可以转化为 nums[i]-i>=nums[j]-j,那么就可以只关注 nums[i]-i 这个值 v 了。之后,对于当前值 v,能产生子序列的就是前面值小于等于 v 的位置中的最大值,所以可以考虑用树状数组维护最大值。因为值域很大,所以还需要离散化一下。

之后,和子数组最大累加和一样,若之前的最大值的收益都小于 0,那么就让当前位置单独构成子序列即可。否则,就去树状数组里更新最大值即可。

三、方伯伯的玉米田

逆天状态设计......

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

const int MAXV=5500+5;
const int MAXK=500+5;

vector<vector<int>>tree(MAXV,vector<int>(MAXK));

int n,k;

int lowbit(int i)
{
    return i&-i;
}

void update(int x,int y,int v)
{
    for(int i=x;i<MAXV;i+=lowbit(i))
    {
        for(int j=y;j<=k+1;j+=lowbit(j))
        {
            tree[i][j]=max(tree[i][j],v);
        }
    }
}

int query(int x,int y)
{
    int ans=0;
    for(int i=x;i>0;i-=lowbit(i))
    {
        for(int j=y;j>0;j-=lowbit(j))
        {
            ans=max(ans,tree[i][j]);
        }
    }
    return ans;
}

void solve()
{
    cin>>n>>k;
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }

    //因为是最长不下降子序列,所以在执行区间加的操作时
    //若从某个位置开始增加,那么必然需要一直增加到结尾
    //所以,右侧数的+1次数必然大于等于左侧数的+1次数
    
    //考虑定义dp[v][j]为已经执行过j次操作,以数值v结尾的最长不下降子序列长度
    //所以若当前数值为v,那么就能更新j=0~k,dp[v+j][j]的格子
    //那么就有转移dp[v][j]=1+max(x=1~v,p=0~j)dp[x][p])
    //所以这个枚举过程可以用二维树状数组优化
    //注意要把j偏移1个位置

    for(int i=1;i<=n;i++)
    {
        //倒序枚举 -> 避免自己产生干扰
        for(int j=k;j>=0;j--)
        {
            int v=a[i]+j;
            int dp=query(v,j+1)+1;

            update(v,j+1,dp);
        }
    }

    cout<<query(MAXV-1,k+1)<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

首先,稍微感觉一下(bushi)就能发现,因为是最长不下降子序列,所以在执行区间加的操作时,若要从某个位置开始增加,那么必然需要一直增加到结尾,否则就会导致求最长不下降子序列的这个问题变得更困难。所以,右侧数的 +1 次数必然大于等于左侧数的 +1 次数。

之后,考虑定义 dp[v][j] 为已经执行过 j 次操作,以数值 v 结尾的最长不下降子序列长度。所以若当前数为 v,那么就能更新 j=0~k,dp[v+j][j] 的格子。所以就依赖 1+max(x=1~v,p=0~j)dp[x][p]),这个枚举过程可以用二维树状数组优化,注意要把 j 偏移 1 个位置。那么最后只需要返回以所有数值 v 结尾,用了 0~k 次的最大值即可。

需要注意的是,在每次枚举操作次数 j 的时候,需要倒序枚举,否则之前更新过的值会产生干扰。

四、最长理想子序列

cpp 复制代码
typedef long long ll;

template<typename T>
struct Segment_Tree
{
    int n;
    //自定义
    vector<T>tree;

    Segment_Tree(int _n){
        n=_n-1;
        tree.resize(_n<<2);
    }

    //初始全0不用build
    void build(const vector<T>&a,int l,int r,int i)
    {
        if(l==r){
            //自定义
            tree[i]=a[l];
        }   
        else{
            int m=(l+r)>>1;
            build(a,l,m,i<<1);
            build(a,m+1,r,i<<1|1);
            up(i);
        }
        //自定义

    }

    void add(int jobl,int jobr,T jobv,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            addLazy();
        }
        else{
            int m=(l+r)>>1;

            down();

            if(jobl<=m){
                add(jobl,jobr,jobv,l,m,i<<1);
            }
            if(m+1<=jobr){
                add(jobl,jobr,jobv,m+1,r,i<<1|1);
            }

            up(i);
        }
    }

    void change(int jobl,int jobr,T jobv,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            updateLazy(jobv,i);
        }
        else{
            int m=(l+r)>>1;

            down();

            if(jobl<=m){
                change(jobl,jobr,jobv,l,m,i<<1);
            }
            if(m+1<=jobr){
                change(jobl,jobr,jobv,m+1,r,i<<1|1);
            }

            up(i);
        }
    }

    T query(int jobl,int jobr,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            return tree[i];
        }
        
        int m=(l+r)>>1;

        down();

        //自定义
        T ans=0;
        if(jobl<=m){
            ans=max(ans,query(jobl,jobr,l,m,i<<1));
        }
        if(m+1<=jobr){
            ans=max(ans,query(jobl,jobr,m+1,r,i<<1|1));
        }

        return ans;
    }

    //自定义
    void addLazy(){

    }

    //自定义
    void updateLazy(T v,int i){
        tree[i]=v;
    }

    //自定义
    void up(int i){
        tree[i]=max(tree[i<<1],tree[i<<1|1]);
    }

    //自定义
    void down(){

    }
};

class Solution {
public:
    int longestIdealString(string s, int k) {
        int n=s.length();
        s=" "+s;

        //定义dp[v]为当前以v结尾的最长合法子序列的长度
        //那么对于当前字符v,合法的转移就是dp[v-k~v+k]
        //所以可以考虑用线段树维护,那么即使k很大也是ok的了

        int ans=0;
        Segment_Tree<int>tree(26+1);
        for(int i=1;i<=n;i++)
        {
            int v=s[i]-'a'+1;

            int l=max(v-k,1);
            int r=min(v+k,26);

            int dp=tree.query(l,r,1,26,1)+1;
            ans=max(ans,dp);

            tree.change(v,v,dp,1,26,1);
        }
        return ans;
    }
};

首先,这个状态肯定是要既包含当前位置和最后一个字符,也就是 dp[i][v] 的形式。又可以发现,在递推的过程中其实不关心是从哪个位置转移过来的。那么就可以省略第一维,直接定义 dp[v] 为当前以 v 结尾的最长合法子序列的长度。所以对于当前字符 v,合法的转移就是 dp[v-k~v+k] 中的最大值。那么对于区间查询和单点修改操作,就可以考虑用线段树维护了,那么即使 k 很大也是可以的了。

五、B. The Bakery

cf的题还是太超模了......

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Segment_Tree
{
    vector<T>tree;
    //自定义
    vector<T>info;

    Segment_Tree(int n){
        tree.resize(n<<2);
        info.resize(n<<2);
    }

    void clear(int l,int r,int i)
    {
        if(l==r){
            tree[i]=0;
        }
        else{
            int m=(l+r)>>1;
            clear(l,m,i<<1);
            clear(m+1,r,i<<1|1);
        }
        //自定义
        info[i]=0;
    }

    //初始全0不用build
    void build(int l,int r,int i,const vector<T>&a)
    {
        if(l==r){
            //自定义
            tree[i]=a[l];
        }
        else{
            int m=(l+r)>>1;
            build(l,m,i<<1,a);
            build(m+1,r,i<<1|1,a);
            up(i);
        }
        //自定义
        info[i]=0;
    }

    void add(int jobl,int jobr,T jobv,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            addLazy(i,jobv);
        }
        else{
            int m=(l+r)>>1;

            down(i);

            if(jobl<=m){
                add(jobl,jobr,jobv,l,m,i<<1);
            }
            if(m+1<=jobr){
                add(jobl,jobr,jobv,m+1,r,i<<1|1);
            }

            up(i);
        }
    }

    // void change(int jobl,int jobr,T jobv,int l,int r,int i){
    //     if(jobl<=l&&r<=jobr){
    //         updateLazy();
    //     }
    //     else{
    //         int m=(l+r)>>1;

    //         down();

    //         if(jobl<=m){
    //             change(jobl,jobr,jobv,l,m,i<<1);
    //         }
    //         if(m+1<=jobr){
    //             change(jobl,jobr,jobv,m+1,r,i<<1|1);
    //         }

    //         up();
    //     }
    // }

    T query(int jobl,int jobr,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            return tree[i];
        }
        
        int m=(l+r)>>1;

        down(i);

        //自定义
        T ans=0;
        if(jobl<=m){
            ans=max(ans,query(jobl,jobr,l,m,i<<1));
        }
        if(m+1<=jobr){
            ans=max(ans,query(jobl,jobr,m+1,r,i<<1|1));
        }

        return ans;
    }

    //自定义
    void addLazy(int i,T v){
        tree[i]+=v;
        info[i]+=v;
    }

    //自定义
    void updateLazy(){

    }

    //自定义
    void up(int i){
        tree[i]=max(tree[i<<1],tree[i<<1|1]);
    }

    //自定义
    void down(int i){
        if(info[i])
        {
            addLazy(i<<1,info[i]);
            addLazy(i<<1|1,info[i]);
            info[i]=0;
        }
    }
};

void solve()
{
    int n,k;
    cin>>n>>k;
    vector<int>a(n+1);
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }

    //定义dp[p][i]为1~i范围上,最多分成p段的最大得分
    //当i<p时,数字个数小于段数,那么就直接等于dp[p-1][i]
    //否则,就枚举最后一段的范围,即依赖max{dp[p-1][ii=0~i-1]+ii~i的种类}

    //考虑优化枚举行为,假如数组为[a a a b b b a]
    //对于dp[p][i],其依赖dp[p-1][0~i-1]
    //首先,dp[p-1][0]必然等于0
    //之后,dp[p][1]由于1位置必然新增一个字符,所以需要在dp[p-1][0]的基础上+1
    //在往后的过程中,由于1~i范围必然有1位置字符,所以这个贡献可以一直推下去
    //之后,dp[p][2]由于2位置和1位置的字符相同,所以dp[p-1][0]不会增加1
    //而因为2位置新增一个字符,所以dp[p-1][1]要+1
    //类似地,dp[p][3]也只在dp[p-1][2]处+1
    //之后,因为4位置的字符和前面的字符都不同,所以dp[p-1][0~3]都需要+1
    //即在上一次b字符出现的位置~当前位置-1范围上都需要+1
    //所以范围增加和范围查询可以用线段树维护

    Segment_Tree<int>tree(n+1);

    //滚动更新
    vector<int>dp(n+1);
    for(int p=1;p<=k;p++)
    {
        //注意范围0~n
        tree.build(0,n,1,dp);

        //每个数上次出现的位置
        vector<int>pre(n+1);
        for(int i=1;i<=n;i++)
        {
            tree.add(pre[a[i]],i-1,1,0,n,1);

            if(i>=p)
            {
                dp[i]=tree.query(0,i-1,0,n,1);
            }
            pre[a[i]]=i;
        }
    }
    
    cout<<dp[n]<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

状态设计倒是比较好想,就是定义 dp[p][i] 为 1~i 范围上,最多分成 p 段的最大得分。那么首先,当 i<p,即数字个数小于段数时,那么就直接等于 dp[p-1][i]。否则,就枚举最后一段的范围,即依赖 max{dp[p-1][j=0~i-1]+j~i的种类}。

之后,考虑优化这个枚举行为,假如数组为 [a a a b b b a],对于 dp[p][i],其依赖 dp[p-1][0~i-1]。首先,dp[p-1][0] 必然等于 0。再之后,dp[p][1] 由于 1 位置必然新增一个字符,所以需要在 dp[p-1][0] 的基础上加一。在往后的过程中,由于 1~i 范围必然有 1 位置字符,所以这个贡献可以一直推下去。之后,dp[p][2] 由于 2 位置和 1 位置的字符相同,所以 dp[p-1][0] 不会增加 1。而因为 2 位置新增一个字符,所以 dp[p-1][1] 要加一。类似地,dp[p][3] 也只在 dp[p-1][2] 处加一。再往后,因为 4 位置的字符和前面的字符都不同,所以 dp[p-1][0~3] 都需要加一,即在上一次 b 字符出现的位置~当前位置 -1 的范围上都需要加一,所以这个范围增加和范围查询可以用线段树维护。

六、基站选址

太变态了......

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

template<typename T>
struct Segment_Tree
{
    vector<T>tree;
    //自定义
    vector<T>info;

    Segment_Tree(int n){
        tree.resize(n<<2);
        info.resize(n<<2);
    }

    //初始全0不用build
    void build(int l,int r,int i,const vector<T>&a)
    {
        if(l==r){
            //自定义
            tree[i]=a[l];
        }   
        else{
            int m=(l+r)>>1;
            build(l,m,i<<1,a);
            build(m+1,r,i<<1|1,a);
            up(i);
        }
        //自定义
        info[i]=0;
    }

    void add(int jobl,int jobr,T jobv,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            addLazy(i,jobv);
        }
        else{
            int m=(l+r)>>1;

            down(i);

            if(jobl<=m){
                add(jobl,jobr,jobv,l,m,i<<1);
            }
            if(m+1<=jobr){
                add(jobl,jobr,jobv,m+1,r,i<<1|1);
            }

            up(i);
        }
    }

    // void change(int jobl,int jobr,T jobv,int l,int r,int i){
    //     if(jobl<=l&&r<=jobr){
    //         updateLazy();
    //     }
    //     else{
    //         int m=(l+r)>>1;

    //         down();

    //         if(jobl<=m){
    //             change(jobl,jobr,jobv,l,m,i<<1);
    //         }
    //         if(m+1<=jobr){
    //             change(jobl,jobr,jobv,m+1,r,i<<1|1);
    //         }

    //         up();
    //     }
    // }

    T query(int jobl,int jobr,int l,int r,int i){
        if(jobl<=l&&r<=jobr){
            return tree[i];
        }
        
        int m=(l+r)>>1;

        down(i);

        //自定义
        T ans=INFLL;
        if(jobl<=m){
            ans=min(ans,query(jobl,jobr,l,m,i<<1));
        }
        if(m+1<=jobr){
            ans=min(ans,query(jobl,jobr,m+1,r,i<<1|1));
        }

        return ans;
    }

    //自定义
    void addLazy(int i,T v){
        tree[i]+=v;
        info[i]+=v;
    }

    //自定义
    void updateLazy(){

    }

    //自定义
    void up(int i){
        tree[i]=min(tree[i<<1],tree[i<<1|1]);
    }

    //自定义
    void down(int i){
        if(info[i])
        {
            addLazy(i<<1,info[i]);
            addLazy(i<<1|1,info[i]);
            info[i]=0;
        }
    }
};

const int MAXN=2e4+5;

int n,k;
vector<ll>dis(MAXN);
vector<ll>cost(MAXN);
vector<ll>range(MAXN);
vector<ll>fix(MAXN);

vector<int>L(MAXN);
vector<int>R(MAXN);
vector<vector<int>>alert(MAXN);

//找大于等于v的最左位置
int bs1(ll v)
{
    int l=1;
    int r=n;
    int m;
    int ans=0;
    while(l<=r)
    {
        m=l+r>>1;
        if(dis[m]>=v)
        {
            ans=m;
            r=m-1;
        }
        else
        {
            l=m+1;
        }
    }
    return ans;
}

//找小于等于v的最右位置
int bs2(ll v)
{
    int l=1;
    int r=n;
    int m;
    int ans=0;
    while(l<=r)
    {
        m=l+r>>1;
        if(dis[m]<=v)
        {
            ans=m;
            l=m+1;
        }
        else
        {
            r=m-1;
        }
    }
    return ans;
}

void build()
{
    for(int i=1;i<=n;i++)
    {
        L[i]=bs1(dis[i]-range[i]);
        R[i]=bs2(dis[i]+range[i]);

        alert[R[i]].push_back(i);
    }
}

vector<ll>dp(MAXN);

void solve()
{
    cin>>n>>k;
    for(int i=2;i<=n;i++)
    {
        cin>>dis[i];
    }
    for(int i=1;i<=n;i++)
    {
        cin>>cost[i];
    }
    for(int i=1;i<=n;i++)
    {
        cin>>range[i];
    }
    for(int i=1;i<=n;i++)
    {
        cin>>fix[i];
    }

    //定义dp[t][i]为最多建t个基站,且最右基站的位置在i号村,1~i号村的最少花费
    //存在单调性,随着最多建造个数t的增加,dp值单调不减
    //考虑在最后补一个村庄,补一个基站,那么最后的答案就是dp[k+1][n+1]
    
    //根据定义,dp[t][i]肯定可以从dp[t-1][i]转移过来
    //之后枚举倒数第二个基站建在哪,那么就是min(dp[t-1][1~i-1]+fix)+cost[i]
    
    //考虑优化枚举
    //定义left[i]为最左在几号村庄建基站,i号村庄还能有信号
    //定义right[i]为最右在几号村庄建基站,i号村庄还能有信号
    //定义alert[i]为i号村庄的预警列表
    //表示只有一个基站在i号村,如果基站移动到i+1号村,有哪些村庄会从有信号变成无信号

    //在从dp[t][i]到dp[t][i+1]转移时,就需要用到alert[i]
    //若alert[i]中有a,若left[a]=p
    //那么在转移时,dp[t-1][1~p-1]都需要加上fix[a]
    //总时间复杂度O(k*n*logn)

    //补一个
    dis[++n]=INFLL;
    cost[n]=range[n]=fix[n]=0;

    //预处理
    build();

    //因为在补的村庄上一定建基站,所以t=1等效t=0
    //初始化t=1
    ll sum=0;
    for(int i=1;i<=n;i++)
    {   
        dp[i]=sum+cost[i];

        for(auto p:alert[i])
        {
            sum+=fix[p];
        }
    }

    Segment_Tree<ll>tree(n+1);

    //dp
    for(int t=2;t<=k+1;t++)
    {
        tree.build(1,n,1,dp);

        for(int i=1;i<=n;i++)
        {
            if(i>=t)
            {
                dp[i]=min(dp[i],tree.query(1,i-1,1,n,1)+cost[i]);
            }

            for(auto p:alert[i])
            {
                if(L[p]>1)
                {
                    tree.add(1,L[p]-1,fix[p],1,n,1);
                }
            }
        }
    }

    cout<<dp[n]<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

考虑定义 dp[t][i] 为最多建 t 个基站,且最右基站的位置在 i 号村,1~i 号村的最少花费。首先,为了方便计算答案,可以考虑在最后补一个村庄补一个基站,那么最后的答案就是 dp[k+1][n+1]。之后可以发现,存在单调性:随着最多建造个数 t 的增加,dp 值是单调不减的。

根据定义,因为是最多,所以 dp[t][i] 肯定可以从 dp[t-1][i] 转移过来。之后,考虑枚举倒数第二个基站建在哪,那么就是 min(dp[t-1][1~i-1]+fix)+cost[i],那么就需要把这个枚举优化掉。

定义 left[i] 为最左在几号村庄建基站,i 号村庄还能有信号,right[i] 为最右在几号村庄建基站,i 号村庄还能有信号,这两个是可以通过二分来解决的。之后,再定义 alert[i] 为 i 号村庄的预警列表,表示只有一个基站在 i 号村时,如果基站移动到 i+1 号村,有哪些村庄会从有信号变成无信号,那么每次就是往 alert[right[i]] 中加入 i 号村庄。

那么在从 dp[t][i] 到 dp[t][i+1] 转移时,就可以用到 alert[i] 了。若 alert[i] 中有 a,若 left[a]=p,那么在转移时,dp[t-1][1~p-1] 在转移时就都需要加上 fix[a]。所以这个范围增加操作就可以用线段树维护了,时间复杂度O(k*n*logn)。

总结

还是要多练 dp!!

END

相关推荐
华科大胡子1 小时前
《Effective C++》学习笔记:条款02
c++·编程语言·inline·const·enum·define
tankeven2 小时前
HJ84 统计大写字母个数
c++·算法
YGGP2 小时前
【Golang】LeetCode 53. 最大子数组和
leetcode
I_LPL2 小时前
day32 代码随想录算法训练营 动态规划专题1
java·数据结构·算法·动态规划·hot100·求职面试
我能坚持多久2 小时前
【初阶数据结构03】——单链表专题
数据结构
啊阿狸不会拉杆2 小时前
《机器学习导论》第 19 章 - 机器学习实验的设计与分析
人工智能·python·算法·决策树·机器学习·统计检验·评估方法
革凡成圣2112 小时前
回忆大一[特殊字符]
数据结构
踩坑记录2 小时前
leetcode hot100 236.二叉树的最近公共祖先 medium dfs 递归
leetcode·深度优先
一马平川的大草原2 小时前
读书笔记--秒懂算法:用常识解读数据结构与算法阅读与记录
数据结构·算法·大o