线段树维护dp状态转移

第五届新疆省赛 L最优子区间

题目描述

给长度为 n 的序列 a[],一个区间的得分为这个区间内有多少种元素恰出现一次。输出得分最大的区间,得分为多少。

分析

  1. 如果暴力枚举所有区间那么是N^2,需要考虑如何优化,可以根据区间得分的特点去思考。
  2. 设dp[l][r-1]为区间l~r-1的得分,那么dp[l][r]的得分有3种情况

d p [ l ] [ r ] = d p [ l ] [ r − 1 ] + 1 , 第 r 个 数 没 有 在 l ∼ r − 1 出 现 过 dp[l][r] = dp[l][r-1] + 1,第 r 个数没有在l \sim r - 1出现过 dp[l][r]=dp[l][r1]+1rlr1
d p [ l ] [ r ] = d p [ l ] [ r − 1 ] − 1 , 第 r 个 数 恰 好 在 l ∼ r − 1 出 现 过 一 次 dp[l][r] = dp[l][r - 1] - 1, 第 r 个数恰好在l \sim r - 1出现过一次 dp[l][r]=dp[l][r1]1,rlr1
d p [ l ] [ r ] = d p [ l ] [ r − 1 ] , 第 r 个 数 恰 好 在 l ∼ r − 1 至 少 出 现 了 2 次 dp[l][r] = dp[l][r - 1] , 第 r 个数恰好在l \sim r - 1至少出现了2次 dp[l][r]=dp[l][r1],rlr12

根据这个特点,不难发现只要记录第r个数上一次出现位置就可以知道新的dp[l][r] 的值,

d p [ l ] [ r ] = d p [ l ] [ r − 1 ] + 1 , 第 r 个 数 上 一 次 出 现 位 置 + 1 ≤ l ≤ r dp[l][r] = dp[l][r-1] + 1, 第r个数上一次出现位置 + 1\leq l \leq r dp[l][r]=dp[l][r1]+1,r+1lr
d p [ l ] [ r ] = d p [ l ] [ r − 1 ] − 1 , 第 r 个 数 上 上 次 出 现 位 置 + 1 ≤ l ≤ 第 r 个 数 上 一 次 出 现 位 置 dp[l][r] = dp[l][r - 1] - 1, 第r个数上上次出现位置 + 1\leq l \leq 第r个数上一次出现位置 dp[l][r]=dp[l][r1]1,r+1lr
d p [ l ] [ r ] = d p [ l ] [ r − 1 ] , l ≤ 第 r 个 数 上 上 次 出 现 位 置 dp[l][r] = dp[l][r-1] , l \leq第r个数上上次出现位置 dp[l][r]=dp[l][r1],lr

举个例子
区间 1 2 3 1 2 3 1 2 3
下标 1 2 3 4 5 6 7 8 9
那么dp[l][9]如下

d p [ l ] [ 9 ] = d p [ l ] [ 8 ] + 1 , 7 ≤ l ≤ 9 dp[l][9] = dp[l][8] + 1, 7 \leq l \leq 9 dp[l][9]=dp[l][8]+1,7l9
d p [ l ] [ 9 ] = d p [ l ] [ r ] − 1 , 3 + 1 < = l < = 6 dp[l][9] = dp[l][r] - 1, 3 + 1<= l <= 6 dp[l][9]=dp[l][r]1,3+1<=l<=6
d p [ l ] [ 9 ] = d p [ l ] [ r ] , l < = 3 dp[l][9] = dp[l][r] , l <= 3 dp[l][9]=dp[l][r],l<=3

  1. 这个状态转移可以通过线段树来做,定义t[i].l ~ t[i].r 为区间左端点在 t[i].l ~ t[i].r ,右端点在i的所有区间的得分值,用last[i]记录上一个第i个数出现的位置,plast[i]记录上上个第i个数出现的位置,i从左到右遍历,每次转移后,求下区间最大得分即可。

code

    #include <bits/stdc++.h>
using namespace std;
#define IOS std::ios_base::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);// 快读

typedef long long ll;
const int MAXN = 1e5 + 10;
struct tnode
{
      int l, r;
      ll lzy,ma;
};

ll c[MAXN], w[MAXN];

struct Segment_Tree
{

      tnode t[MAXN << 2] = {};
      int leaf[MAXN] = {};

      inline void update(int i) //用子区间更新当前的区间
      {
            t[i].ma = max(t[i << 1].ma, t[i << 1 | 1].ma);
      }

      void inline init_lzy(int i) //初始化懒标记
      {
            t[i].ma = t[i].lzy = 0;
      }

      void inline build(int i, int l,int r ) // 建树
      {
            t[i].l = l;
            t[i].r = r;

            init_lzy(i); //important
            if (l == r)
            {
                  leaf[t[i].l] = i;
                  return;
            }
            int m = (l + r) >> 1;
            build(i << 1, l, m);
            build(i << 1 | 1, m + 1, r);
            //update(i);
      }
      void push_down(int i){
            if(t[i].lzy){
                  ll lz = t[i].lzy;
                  t[i << 1].ma += lz;
                  t[i << 1].lzy += lz;
                  t[i << 1 | 1].ma += lz;
                  t[i << 1 | 1].lzy += lz;
                  t[i].lzy = 0;
            }
      }

      inline void add(int i, int l, int r, ll b)
      {
            if (t[i].l >= l && t[i].r <= r)
            {
                  t[i].ma += b;
                  t[i].lzy += b;
                  return ;
            }
            push_down(i);

            if (t[i << 1].r >= l)
                  add(i << 1, l, r, b);
            if (t[i << 1 | 1].l <= r)
                  add(i << 1 | 1, l, r, b);
            update(i);
      }

      inline ll search_sqares(int i, int l, int r)
      {
            if (l <= t[i].l && t[i].r <= r)
            {
                  return t[i].ma;
            }

            push_down(i);

            ll res = 0;

            if (t[i << 1].r >= l)
                  res = search_sqares(i << 1, l, r);
            if (t[i << 1 | 1].l <= r)
                  res = max( res,search_sqares(i << 1 | 1, l, r) );

            
            return res;
      }

      void check_leaf(int sz)
      { //查询叶子是否正确
            for (int i = 1; i <= sz; ++i)
                  if (t[i].l == t[i].r && t[i].l)
                        cout << t[i].l << " " << t[i].r << " " << t[i].ma << '\n';
      }

      void check_section(int sz, int l, int r)
      { //查询区间是否正确
            for (int i = 1; i < sz; ++i)
            {
                  if (l <= t[i].l && t[i].r <= r)
                  {
                        cout << t[i].l << ' ' << t[i].r << '\n';
                  }
            }
      }
};

Segment_Tree ST;
int last[MAXN], plast[MAXN];
int main(){
      int n;
      while(cin >> n){
            
            for(int i = 1; i <= n; ++i)
                  cin >> c[i];
            ST.build(1,1,n);
            memset(last,0,sizeof last);
            memset(plast,0,sizeof plast);
            ll ans = 0;
            for(int i = 1; i <= n; ++i){
                  ST.add(1,last[c[i]] + 1,i,1);
                  if(last[c[i]])
                       ST.add(1,plast[c[i]] + 1,last[c[i]],-1);
                 // cout << last[c[i]]  + 1 << " " << i << '\n';
                 // cout << plast[c[i]] + 1 << " " << last[c[i]] << '\n';
                  plast[c[i]] = last[c[i]];
                  last[c[i]] = i;
                  ans = max(ans,ST.search_sqares(1,1,n));
            }
          cout << ans << '\n';
      }
}


方块 III

一道基本一样的题目





进阶题

E. Partition Game

题目描述

You are given an array a of n integers. Define the cost of some array t as follows:

cost(t)=∑x∈set(t)last(x)−first(x),
where set(t) is the set of all values in t without repetitions, first(x), and last(x) are the indices of the first and last occurrence of x in t, respectively. In other words, we compute the distance between the first and last occurrences for each distinct element and sum them up.

You need to split the array a into k consecutive segments such that each element of a belongs to exactly one segment and the sum of the cost of individual segments is minimum.



思路

题意很像,就是更复杂了

  1. 先试着把普通的动态规划代码写出来,在用线段树维护状态转移
dp[k][l][r];// 划分到第k块,第k块,第k块长度为l ~ r


for (int i = 1; i <= k; ++i)
{
    memset(last, 0x3f, sizeof last);

    for (int j = i; j <= n - (k - i); ++j)
    {

        dp[i][j][j] = min(dp[i - 1][...][j - 1]);// i - 1 <= ... <= j - 1
       
    } 

    for (int l = i; l <= n - (k - i); ++l)
    {
        for (int r = l; r <= n - (k - i); ++r)
        {

            dp[i][l][r] += max(r - last[x[r]], 0);
            last[x[r]] = r;
        }
    }
}

    
  1. 然后可以用线段树进行维护状态转移,可以开个大小为2的线段树数组,滚动一下。

    ① 下面代码中 当前块继承前面块的答案,可以放在枚举区间左右端点的时候做

for (int j = i; j <= n - (k - i); ++j)
    {

        dp[i][j][j] = min(dp[i - 1][...][j - 1]);// i - 1 <= ... <= j - 1
       
    } 

②下面代码 枚举块的左右端点可以用线段树做。

for (int l = i; l <= n - (k - i); ++l)
    {
        for (int r = l; r <= n - (k - i); ++r)
        {

            dp[i][l][r] += max(r - last[x[r]], 0);
            last[x[r]] = r;
        }
    }




AC代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MAXN = 35020,inf = 1e9;
struct tnode
{
    ll l, r,lzy,mi;// mi 区间最小值
};

ll last[MAXN],a[MAXN];

struct Segment_Tree
{

    tnode t[MAXN << 2] = {};
    int leaf[MAXN] = {};// 存叶子结点在t数组的下标
    

    inline void update(int i)//用子区间更新当前的区间
    {
            t[i].mi = min(t[i << 1].mi,t[i << 1 | 1].mi);
    
    }
     inline void renew(int i)//i为叶子结点下标,当修改叶子结点后,从叶子结点更新父区间直到根节点
    {
        while (i >>1 )
        {
            i >>= 1;
            t[i].mi = min(t[i << 1].mi , t[i << 1 | 1].mi);
        }
    }

    void inline init_lzy(int i)//初始化懒标记等
    {
        t[i].lzy = 0;
        t[i].mi = 0;
    }

    void inline build(int i, ll l, ll r)// 建树
    {
        t[i].l = l;
        t[i].r = r;
      
        init_lzy(i); //important
        if (l == r)
        {
            leaf[t[i].l] = i;
            return;
        }
        ll m = (l + r) >> 1;
        build(i << 1, l, m);
        build(i << 1 | 1, m + 1, r);
        update(i);
    }

 

    inline void push_down(int i)
    {
        if(t[i].lzy){
              t[i << 1].lzy += t[i].lzy;
              t[i << 1].mi += t[i].lzy;
              t[i << 1 | 1].lzy += t[i].lzy;
              t[i << 1 | 1].mi += t[i].lzy;
              t[i].lzy = 0;
        }
    }

    inline void add(int i, ll l, ll r, ll b)
    {
        if(b <= 0)    return ;
        if (t[i].l >= l && t[i].r <= r)
        {
           t[i].mi += b;
           t[i].lzy += b;
           return ;
        }
        push_down(i);

        
        if (t[i << 1].r >= l)
            add(i << 1, l, r, b);
        if (t[i << 1 | 1].l <= r)
            add(i << 1 | 1, l, r, b);
        update(i);
    }

    
    inline ll search_sqares(int i, int l, int r)// 区间查找
    {
        if (l <= t[i].l && t[i].r <= r)
        {
            return t[i].mi;
        }

        push_down(i);

        ll x = inf;

        if (t[i << 1].r >= l)
            x = search_sqares(i << 1, l, r);
        if (t[i << 1 | 1].l <= r)
            x = min(x,search_sqares(i << 1 | 1, l, r) );

        /*      合并两个区间得到的答案后在返回     */
        return x;
    }

    void check_leaf(int sz){//查询叶子是否正确
        for(int i = 1; i <= sz; ++i)
            if(t[i].l == t[i].r && t[i].l)
            cout << t[i].l << " " << t[i].r << " " << t[i].mi << '\n';
    }

    void check_section(int sz,int l,int r){//查询区间是否正确
        for(int i = 1; i < sz; ++i){
            if(l <= t[i].l && t[i].r <= r){
                cout << t[i].l << ' ' << t[i].r << '\n';
            }
        }
    }

};

Segment_Tree ST[2];


int n,k;
int main()
{
      cin >> n >> k;
      ST[0].build(1,1,n);    ST[1].build(1,1,n); 
      memset(last,0x3f,sizeof last);
      for(int i = 1; i <= n; ++i)
            cin >> a[i];
     
      for (int i = 1; i <= k; ++i)
      {

            // for(int j = i; j <= n - (k - i); ++j){
                  
            //             dp[k][j + 1][j] = min(dp[k - 1][][]); search(1,i - 1,j);
            // }// min( dp[k - 1][...][j] )

            // for (int l = i; l <= n - (k - i); ++l)
            // {
            //       for (int r = l; r <= n - (k - i); ++r)
            //       {
                       
            //                   dp[k][l][r] += max(r - last[x[r]], 0);
            //                   last[x[r]] = r;
            //             
            //       }
            // }
           
            for(int r = i; r <= n - (k - i); ++r){
                  //if(i == 2)
                      //cout << r << " " << i << " " << last[a[r]]  << " " << r - last[a[r]] << '\n';
                  ST[i&1].add( 1,i,last[a[r]], r - last[a[r]] );// 1 <= 第i个区间左端的 <= n - (k - i)
                  int id = ST[ (i + 1) & 1].leaf[min(r  + 1,n)];// 第i + 1 个区间左端的r + 1 的位置,最后会越n的边界
                  // 第i + 1个区间如果在r + 1 开始则要继承从 1 ~ r 分完前 i 个区间后的最小值,特判,如果是第2个区间那么要继承的第1个区间的左端的必须在 1 
                  ST[ (i + 1) & 1].t[id].mi = i != 1 ? ST[i&1].search_sqares(1,i,r) :  ST[i&1].search_sqares(1,i,i);
                  
                  
                  ST[ (i + 1)& 1].renew(id); //获取后从叶子结点向上更新
                  last[a[r]] = r;
            }
         
         
            memset(last,0x3f,sizeof last);     
            // 必须重新建树,相当于初始化,虽然吧结点的mi改了,但是里面的lzy 标签还在,会影响下次add
            if(i!=k)
              ST[i&1].build(1, 1, n+1);
      }
      if(k == 1)// 如果只有一个区间,需要特判选择左端点从1开始
           cout << ST[k&1].search_sqares(1,1,1);
      else cout << ST[k&1].search_sqares(1,k,n);
    
      return 0;
}



//
// 1 ~ last[x] += (r - last[x]);
// last[x] = r;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值