区间合并 单点更新求最长上升子序列

本文介绍了一道经典算法题目——最长连续上升子序列(LCIS)的高效解法。通过使用线段树进行区间更新与查询,实现了动态维护序列状态的功能。适用于动态修改序列并快速查询区间最值的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接hdu3308 LCIS

题目大意:给你n个整数,有两种操作,(1)"U A B",表示把第A个数变成B,"Q A B",表示查询区间[A,B]的最长连续上升序列。

解题思路:O(-1)

#include <cstdio>
#include <cmath>
#include <iostream>
#include <algorithm>
using namespace std;

#define lz 2*u,l,mid
#define rz 2*u+1,mid+1,r
const int maxn=100005;
int a[maxn];

struct node
{
    int lm;  ///定义从左边第一个点开始的LCIS
    int rm;  ///定义以右边最后一个点结束的LCIS
    int sm;  ///定义整段最大的LCIS
}tree[4*maxn];

void push_up(int u, int l, int r)
{
    tree[u].lm=tree[2*u].lm;
    tree[u].rm=tree[2*u+1].rm;
    tree[u].sm=max(tree[2*u].sm,tree[2*u+1].sm);
    int mid=(l+r)>>1;
    if(a[mid]<a[mid+1])
    {
        if(tree[2*u].lm==mid-l+1) tree[u].lm=tree[2*u].lm+tree[2*u+1].lm;
        if(tree[2*u+1].rm==r-mid)  tree[u].rm=tree[2*u].rm+tree[2*u+1].rm;
        int t=tree[2*u].rm+tree[2*u+1].lm;
        if(t>tree[u].sm)  tree[u].sm=t;
    }
}

void build(int u, int l, int r)
{
    if(l==r)
    {
        tree[u].lm=tree[u].rm=tree[u].sm=1;
        return ;
    }
    int mid=(l+r)>>1;
    build(lz);
    build(rz);
    push_up(u,l,r);
}

void Update(int u, int l, int r, int p, int d)
{
    if(l==r)
    {
        a[l]=d;  return ;
    }
    int mid=(l+r)>>1;
    if(p<=mid)  Update(lz,p,d);
    else    Update(rz,p,d);
    push_up(u,l,r);
}

int Query(int u, int l, int r, int tl, int tr)
{
    if(tl<=l&&r<=tr)
    {
        return tree[u].sm;
    }
    int mid=(l+r)>>1;
    if(tr<=mid)  return Query(lz,tl,tr);
    else if(tl>mid) return Query(rz,tl,tr);
    else
    {
        int t1=Query(lz,tl,mid);
        int t2=Query(rz,mid+1,tr);
        int t=max(t1,t2);
        if(a[mid]<a[mid+1])
        {
            t1=min(tree[2*u].rm,mid-tl+1);  ///!!!
            t2=min(tree[2*u+1].lm,tr-mid);  ///!!!
            t1+=t2;
        }
        return max(t,t1);
    }
}

int main()
{
    int n, m, tcase;
    cin >> tcase;
    while(tcase--)
    {
        cin >> n >> m;
        for(int i=1; i<=n; i++)
            scanf("%d",a+i);
        build(1,1,n);
        while(m--)
        {
            char ch[5];
            int l, r;
            scanf("%s%d%d",ch,&l,&r);
            if(ch[0]=='U')
            {
                l++;
                Update(1,1,n,l,r);
            }
            else
            {
                l++, r++;
                int ans=Query(1,1,n,l,r);
                printf("%d\n",ans);
            }
        }
    }
    return 0;
}


<think>好的,我现在需要介绍区间动态规划。用户可能已经了解基础的动态规划,但想进一步学习区间动态规划的应用和实现方法。首先,我需要明确区间动态规划的定义和适用场景。 区间动态规划通常用于解决涉及区间或序列的问题,比如石子合并、最长回文子序列等。这类问题的特点是最优解可以通过子区间的最优解组合得到。关键点在于如何定义状态转移方程,以及如何处理区间的划分。 接下来,我需要解释区间动态规划的基本思想。状态定义一般为dp[i][j],表示区间[i,j]上的最优解。然后,通过枚举区间内的分割点k,将问题分解为子问题dp[i][k]和dp[k+1][j],再根据具体问题合并结果。例如,石子合并中的状态转移方程是dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum(i,j)),其中sum是合并的代价。 然后,我应该说明实现步骤。首先确定状态表示,然后初始化基础情况(通常是长度为1的区间),接着按照区间长度递增的顺序进行递推,逐步计算更大区间的解。需要注意循环的顺序,通常外层循环是区间长度,中层是起始点i,内层是分割点k。 用户可能需要一个具体的例子来理解。比如石子合并问题,给定一堆石子的重量,每次合并相邻两堆,代价是它们的重量之和,最小总代价。代码实现时,需要预处理前缀和数组,方便计算区间和。然后按照上述步骤填充dp数组。 可能需要注意的易错点包括区间划分是否正确,循环顺序是否正确,以及初始化是否正确。比如,当处理回文子序列时,状态转移可能涉及两端字符是否相等的情况,这时候需要分情况讨论。 另外,时间复杂度也是一个需要考虑的因素。区间动态规划通常的时间复杂度是O(n^3),因为有三层循环。对于较大的n可能会有性能问题,需要优化或者考虑其他方法。 最后,总结区间动态规划的适用条件:问题可以分解为相互重叠的子区间问题,且具有最优子结构。常见的题目包括矩阵链乘法、最优二叉搜索树、字符串相关的问题等。 现在,我需要将这些内容组织成结构清晰的解释,并给出代码示例,如石子合并的C++实现。确保术语准确,逻辑连贯,帮助用户逐步理解区间动态规划的应用方法。</think>--- ### **区间动态规划核心概念** **区间DP**用于解决涉及区间操作的问题,通过将大区间分解为小区间解最优解。其核心特征: - **状态定义**:通常使用$dp[i][j]$表示区间$[i,j]$上的最优解 - **递推方式**:通过枚举分割点$k$,合并子区间结果$dp[i][k]$和$dp[k+1][j]$ - **常见问题**:石子合并、回文子序列、矩阵链乘法等 --- ### **算法框架** #### **1. 状态定义** $$ dp[i][j] = \text{区间}[i,j]\text{上的最优解} $$ #### **2. 转移方程(以石子合并为例)** $$ dp[i][j] = \min_{k=i}^{j-1}(dp[i][k] + dp[k+1][j]) + \sum_{t=i}^{j} w[t] $$ #### **3. 实现步骤** 1. **预处理**:计算前缀和数组(快速获取区间和) 2. **初始化**:长度为1的区间$dp[i][i] = 0$ 3. **递推顺序**:按区间长度从小到大计算 --- ### **经典案例:石子合并** #### **问题描述** 有$N$堆石子排成一列,每次合并相邻两堆,代价为两堆重量之和,合成一堆的最小总代价。 #### **C++代码实现** ```cpp #include <iostream> #include <vector> #include <climits> using namespace std; int main() { int n; cin >> n; vector<int> w(n+1, 0); // 石子重量 vector<int> prefix(n+1, 0); // 前缀和 for(int i=1; i<=n; i++) { cin >> w[i]; prefix[i] = prefix[i-1] + w[i]; } // dp[i][j]表示合并[i,j]石子的最小代价 vector<vector<int>> dp(n+1, vector<int>(n+1, 0)); // 枚举区间长度(从2开始,长度1无需合并) for(int len=2; len<=n; len++) { for(int i=1; i+len-1<=n; i++) { // 起点i int j = i + len - 1; // 终点j dp[i][j] = INT_MAX; // 枚举分割点k for(int k=i; k<j; k++) { int cost = dp[i][k] + dp[k+1][j] + prefix[j] - prefix[i-1]; dp[i][j] = min(dp[i][j], cost); } } } cout << "最小合并代价: " << dp[1][n] << endl; return 0; } ``` --- ### **代码解析** 1. **输入处理**:读入石子重量并计算前缀和数组`prefix` 2. **DP数组初始化**:$dp[i][i] = 0$(合并单堆石子无需代价) 3. **三重循环结构**: - **外层**:遍历区间长度(从小到大的关键) - **中层**:确定区间起点$i$ - **内层**:枚举分割点$k$,计算合并代价 4. **状态转移**:$dp[i][j] = \min(dp[i][k] + dp[k+1][j]) + \text{区间和}$ --- ### **时间复杂度分析** - **预处理**:$O(n)$ - **动态规划**:三重循环导致$O(n^3)$ - **适用场景**:通常$n \leq 300$时可通过 --- ### **关键点总结** 1. **区间划分顺序**:必须按长度从小到大递推 2. **前缀和优化**:快速计算区间合并代价 3. **边界处理**:特别注意$i$和$j$的取值界限 4. **问题变形**:环形石子合并可通过数组翻倍转换为线性问题 --- ### **典型应用场景** 1. **最长回文子序列**:$dp[i][j]$表示区间内最长回文长度 2. **矩阵链乘法**:最小化矩阵相乘的计算次数 3. **括号匹配优化**:计算最大有效括号子序列长度 通过合理定义状态和转移方程,区间DP能高效解决多种区间最优问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值