线段树维护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;

### 树链剖分与动态规划的结合用法 树链剖分是一种用于处理树上路径查询和修改的技术,通过两次深度优先搜索 (DFS) 将树转化为线性区间,并利用数据结构(如线段树或树状数组)加速操作。动态规划则可以通过预处理子问题的答案来减少冗余计算。两者的结合主要体现在对树上的某些属性进行快速更新和查询时,使用动态规划的思想优化状态转移。 以下是具体的实现方法以及代码示例: #### 实现原理 1. **树链剖分的核心** 使用两次 DFS 完成轻重边划分,将树划分为若干条重链。每条重链对应一个连续的编号范围,便于后续在线段树或其他数据结构中进行区间操作[^4]。 2. **动态规划的状态定义** 假设我们需要维护某种树节点之间的关系(例如最大值、最小值或总和),可以定义 DP 状态 `dp[u]` 表示以节点 `u` 为根的子树中的某个最优解。对于不同类型的题目,DP 的具体含义会有所不同。 3. **状态转移方程** 利用树链剖分后的父子关系,可以从父节点向子节点传递信息,或者反过来从子节点向上汇总到父节点。这种自底向上的方式非常适合动态规划的应用场景。 4. **结合线段树/树状数组** 如果需要频繁地对某条路径上的数值进行修改或查询,则可以在树链剖分的基础上引入线段树等辅助工具,进一步提升效率。 --- #### 代码示例:树链剖分 + 动态规划 下面是一个典型的例子——在树上求路径的最大权值和。我们将结合树链剖分和动态规划完成这一目标。 ```python from collections import defaultdict class TreeChainDecomposition: def __init__(self, n): self.n = n self.graph = [[] for _ in range(n)] self.parent = [-1] * n self.depth = [0] * n self.size = [0] * n self.heavy = [-1] * n self.chain_idx = [0] * n self.pos_in_base = [0] * n self.base_array = [] def add_edge(self, u, v): self.graph[u].append(v) self.graph[v].append(u) def first_dfs(self, root=0): # 计算 size 和 heavy 子节点 stack = [(root, -1)] while stack: node, prev_node = stack.pop() if prev_node != -1 and self.parent[node] == -1: self.parent[node] = prev_node self.depth[node] = self.depth[prev_node] + 1 sub_size = 0 max_subtree = -1 for child in self.graph[node]: if child != prev_node: stack.append((child, node)) sub_size += 1 if max_subtree == -1 or self.size[child] > self.size[max_subtree]: max_subtree = child self.size[node] = sub_size + 1 self.heavy[node] = max_subtree def second_dfs(self, root=0, chain_root=-1): # 给定重链索引并分配 base 数组位置 pos = len(self.base_array) self.chain_idx[root] = chain_root if chain_root != -1 else root self.pos_in_base[root] = pos self.base_array.append(root) if self.heavy[root] != -1: # 处理重儿子 self.second_dfs(self.heavy[root], self.chain_idx[root]) for child in self.graph[root]: # 非重儿子单独形成新链 if child != self.parent[root] and child != self.heavy[root]: self.second_dfs(child, child) def update_segment_tree(tree, idx, value, start, end, seg_pos=1): if start == end: tree[seg_pos] = value return mid = (start + end) // 2 if idx <= mid: update_segment_tree(tree, idx, value, start, mid, seg_pos*2) else: update_segment_tree(tree, idx, value, mid+1, end, seg_pos*2+1) tree[seg_pos] = max(tree[seg_pos*2], tree[seg_pos*2+1]) def query_segment_tree(tree, l, r, start, end, seg_pos=1): if l > end or r < start: return float('-inf') if l <= start and end <= r: return tree[seg_pos] mid = (start + end) // 2 left_query = query_segment_tree(tree, l, r, start, mid, seg_pos*2) right_query = query_segment_tree(tree, l, r, mid+1, end, seg_pos*2+1) return max(left_query, right_query) # 初始化树和输入样例 n = 8 edges = [[0, 1], [0, 2], [1, 3], [1, 4], [2, 5], [2, 6], [6, 7]] weights = {i: i+1 for i in range(n)} # 节点权重 tcd = TreeChainDecomposition(n) for u, v in edges: tcd.add_edge(u, v) tcd.first_dfs() # 第一次 DFS 构建大小和重儿子信息 tcd.second_dfs() # 第二次 DFS 进行链划分 segment_tree = [float('-inf')] * (len(tcd.base_array)*4) # 创建线段树 for i in range(len(tcd.base_array)): update_segment_tree(segment_tree, i, weights[tcd.base_array[i]], 0, len(tcd.base_array)-1) # 查询路径最大值函数 def query_path_max(node_u, node_v): result = float('-inf') while tcd.chain_idx[node_u] != tcd.chain_idx[node_v]: if tcd.depth[tcd.chain_idx[node_u]] < tcd.depth[tcd.chain_idx[node_v]]: node_u, node_v = node_v, node_u current_chain_head = tcd.chain_idx[node_u] pos_start = tcd.pos_in_base[current_chain_head] pos_end = tcd.pos_in_base[node_u] result = max(result, query_segment_tree(segment_tree, pos_start, pos_end, 0, len(tcd.base_array)-1)) node_u = tcd.parent[current_chain_head] last_chain_head = tcd.chain_idx[node_u] pos_start = min(tcd.pos_in_base[node_u], tcd.pos_in_base[node_v]) pos_end = max(tcd.pos_in_base[node_u], tcd.pos_in_base[node_v]) result = max(result, query_segment_tree(segment_tree, pos_start, pos_end, 0, len(tcd.base_array)-1)) return result # 测试查询 print(query_path_max(0, 7)) # 输出路径最大值 ``` 上述代码展示了如何结合树链剖分和动态规划解决问题的过程。其中,动态规划的部分体现在对每个节点的最终权值进行了预先存储,并通过线段树支持高效的区间查询[^5]。 --- ### 总结 树链剖分提供了强大的工具来简化树形结构的操作,而动态规划能够有效降低复杂度。两者结合后,在许多实际应用中表现出色,尤其是在涉及大量路径查询的情况下。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值