【训练题50:动态规划 + 单调队列】League of Legends | 2021牛客暑期多校训练营2

该博客介绍了如何使用动态规划解决一个关于线段区间分组的问题,目标是最大化组间线段交集的长度。首先通过排序和筛选排除包含关系的线段,然后对剩余线段进行动态规划状态转移,利用单调队列优化查找最大贡献。最终,通过处理大线段来完成所有情况的最大值计算。
摘要由CSDN通过智能技术生成

题意

  • League of Legends | 2021牛客暑期多校训练营2
    n n n 段区间,第 i i i 段为 [ L i , R i ) [L_i,R_i) [Li,Ri)
    把它们分成 k k k 组,第 i i i 组的贡献为这组中线段的交的长度(要求非空)
    问你总贡献最大是多少
  • 1 ≤ k ≤ n ≤ 5000 1\le k\le n\le 5000 1kn5000
    0 ≤ L i < R i < 1 0 5 0\le L_i<R_i<10^5 0Li<Ri<105

思路

  • 首先可能会想到一个二维 d p dp dp ,设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 条线段分成 j j j 组的最大贡献,已经是 O ( n k ) O(nk) O(nk)
    但是我们不知道第 j j j 组的线段交的左右边界如何,也存不下这些状态了
    然后貌似就卡在这里了,是因为我们想的线段之间的关系比较复杂
  • 如果有线段 i , j i,j i,j 满足 i ∩ j = i i\cap j=i ij=i,那么显然线段 j j j 包含了线段 i i i
    假设我们线段 i i i 放在组 x x x ,我们对于线段 j j j ,有两种最优策略
    一种:新开一组,单独放线段 j j j ,答案会增加 j j j 长度的贡献
    另一种:把线段 j j j 放在组 x x x ,答案不会改变 (想一想为什么)
    若把线段 j j j 放在别的组,有可能导致那组的线段交会长度变短,自然更劣
  • 所以我们把所有的包含别的线段大线段拿出来,只考虑小线段分组的情况
    怎么拿呢?看代码:
/**
	seg[i] : 原线段
	sml[i] : 小线段
	big[i] : 大线段的长
*/

struct node{
    int l,r;
    bool operator <(const node &ND)const{
        if(l != ND.l)return l < ND.l;		// 注意重载规则
        return r > ND.r;
    }
}seg[MAX],sml[MAX];
int big[MAX];
int id1,id2;
int main()
{
    IOS;
    int n,k;cin >> n >> k;
    for(int i = 0;i < n;++i)
        cin >> seg[i].l >> seg[i].r;

    sort(seg,seg+n);

    int you = INF;
    for(int i = n-1;~i;--i){		// 我们倒着取,记录目前线段的最右边界
        if(seg[i].r >= you)big[id2++] = seg[i].r - seg[i].l;		// 右区间大于最右边界,是包含别的线段的大线段
        else {
            sml[++id1] = seg[i];
            you = seg[i].r;
        }
    }
}
  • 第一个难点过去了,接下来是第二个难点。
    我们对小线段排序一下,按照左端点升序即可
    因为我们拿出来的所有小线段都满足两两不包含,即 ∀ i , j , i < j \forall i,j,i<j i,j,i<j 我们满足 L i < L i , R i < R j L_i<L_i,R_i<R_j Li<Li,Ri<Rj
    这对我们后续的操作都大有裨益
    接下来是动态规划了。我们设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 条线段分 j j j 组的最大贡献,式子也很好转移了:
    d p [ i ] [ j ] = max ⁡ { d p [ k ] [ j − 1 ] + s m l [ k + 1 ] . r − s m l [ i ] . l ∣ i > k + 1 , s m l [ i ] ∩ s m l [ k + 1 ] ≠ ∅ } dp[i][j]=\max\{ dp[k][j-1] + sml[k+1].r - sml[i].l \Big| i>k+1,sml[i]\cap sml[k+1] \ne \varnothing\} dp[i][j]=max{dp[k][j1]+sml[k+1].rsml[i].li>k+1,sml[i]sml[k+1]=}
    我们可以稍微处理一下式子:
    d p [ i ] [ j ] = max ⁡ { d p [ k − 1 ] [ j − 1 ] + s m l [ k ] . r } − s m l [ i ] . l ∣   i > k , s m l [ i ] ∩ s m l [ k ] ≠ ∅ dp[i][j]=\max\{ dp[k-1][j-1] + sml[k].r \} - sml[i].l \qquad \Big |\ i> k,sml[i]\cap sml[k]\ne \varnothing dp[i][j]=max{dp[k1][j1]+sml[k].r}sml[i].l i>k,sml[i]sml[k]=
    简单讲,就是 k ∼ i k\sim i ki 的线段都分成一组,区间交就是 s m l [ k ] . r − s m l [ i ] . l sml[k].r - sml[i].l sml[k].rsml[i].l
  • 问题来了:怎么快速的去算呢?容易想到,这个满足 单调队列 的性质,即合法区间一定是连续的一段,类似尺取
    然后注意到,对于分 k k k 组,我们需要计算的最大值中是 k − 1 k-1 k1
    所以我们开 k k k 个单调队列去存就好
    另外,就像背包一样,我们组数倒着跑,不然某一段区间可能被连续贡献
  • 还有一个点。对于那些大线段,我们怎么处理?
    假设我们所有的小线段分成了 k − i k-i ki 组,那么大线段要分 i i i 组,我们去拿最长的 i i i 个大线段分这 i i i 组即可,因为别的可以放到前面的小线段的组中,对答案的贡献都是 0 0 0
    然后对所有情况取 max ⁡ \max max 即可

代码

  • 时间复杂度: O ( n k ) O(nk) O(nk)
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
using namespace std;
typedef long long ll;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}

const int MAX = 5e3+50;
const int MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
const double EPS = 1e-5;


struct node{
    int l,r;
    bool operator <(const node &ND)const{
        if(l != ND.l)return l < ND.l;
        return r > ND.r;
    }
}seg[MAX],sml[MAX];
int big[MAX];
int id1,id2;
int dp[MAX][MAX];

deque<int>Q[MAX];
bool capEmpty(int x,int y){
    return sml[y].l >= sml[x].r;
    //return l >= r;
}
int main()
{
    IOS;
    int n,k;cin >> n >> k;
    for(int i = 0;i < n;++i)
        cin >> seg[i].l >> seg[i].r;

    sort(seg,seg+n);

    int you = INF;
    for(int i = n-1;~i;--i){
        if(seg[i].r >= you)big[id2++] = seg[i].r - seg[i].l;
        else {
            sml[++id1] = seg[i];
            you = seg[i].r;
        }
    }

    sort(sml+1,sml+id1+1);
    sort(big,big+id2,greater<int>());

    memset(dp,-1,sizeof(dp));	// -1 表示取不到
    dp[0][0] = 0;
    Q[0].push_back(1);		// 初始状态没有就进不去了
    for(int i = 1;i <= id1;++i){
        for(int j = k;j >= 1;--j){
            while(Q[j-1].size() && capEmpty(Q[j-1].front(),i))Q[j-1].pop_front();

            if(Q[j-1].size())dp[i][j] = dp[Q[j-1].front()-1][j-1] + sml[Q[j-1].front()].r - sml[i].l;

            if(~dp[i][j]){
                while(Q[j].size() && dp[i][j] + sml[i+1].r >= dp[Q[j].back()-1][j] + sml[Q[j].back()].r)Q[j].pop_back();
                Q[j].push_back(i+1);
            }
        }
    }
    ll ans = 0;
    ll sum = 0;
    for(int i = 0;i <= id2 && k - i >= 0;++i){
        if(~dp[id1][k-i])
            ans = max(ans,dp[id1][k - i] + sum);
        sum += big[i];
    }
    cout << ans;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值