CF960F Pathwalks [dp+动态开点线段树]

本文探讨了在一个可能包含重边和自环的有向图中寻找一条路径的问题,该路径需通过最多的边且边的权值严格递增,同时保持输入顺序。采用动态规划与线段树结合的方法,实现高效求解。

F Pathwalks
time limit per test1 second
memory limit per test256 megabytes
inputstandard input
outputstandard output
You are given a directed graph with n nodes and m edges, with all edges having a certain weight.

There might be multiple edges and self loops, and the graph can also be disconnected.

You need to choose a path (possibly passing through same vertices multiple times) in the graph such that the weights of the edges are in strictly increasing order, and these edges come in the order of input. Among all such paths, you need to find the the path that has the maximum possible number of edges, and report this value.

Please note that the edges picked don’t have to be consecutive in the input.

Input
The first line contains two integers n and m (1 ≤ n ≤ 100000,1 ≤ m ≤ 100000) — the number of vertices and edges in the graph, respectively.

m lines follows.

The i-th of these lines contains three space separated integers ai, bi and wi (1 ≤ ai, bi ≤ n, 0 ≤ wi ≤ 100000), denoting an edge from vertex ai to vertex bi having weight wi

Output
Print one integer in a single line — the maximum number of edges in the path.

Examples
inputCopy
3 3
3 1 3
1 2 1
2 3 2
outputCopy
2
inputCopy
5 5
1 3 2
3 2 3
3 4 5
5 4 0
4 5 8
outputCopy
3
Note
The answer for the first sample input is 2: . Note that you cannot traverse because edge appears earlier in the input than the other two edges and hence cannot be picked/traversed after either of the other two edges.

In the second sample, it’s optimal to pick 1-st, 3-rd and 5-th edges to get the optimal answer: .

题意:有n个顶点,m条边,可能存在重边和自环,每条边都有权值wi,要求一条经过边数最多且经过的边的权值是严格递增且经过的边的相对顺序与输入相对顺序一样的路径,输出路径长度

题解:这个相当于图上的最长上升子序列,仿照最长上升子序列可以使用线段树快速找到到达某个结点的最长路径长度,但是由于最长上升子序列的每个元素的顺序是固定的,所以只需要一个建一个线段树即可,而图中顶点的顺序是不定的,所以需要在每个结点上建立一棵线段树,显然直接建树会MLE,需要动态开点建线段树。又由于经过的边顺序需要按照输入的相对顺序,所以可以边输入边更新线段树边更新结果

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define debug(x) cout<<#x<<" is "<<x<<endl;
const int maxn=1e5+5;
struct node{
    int lson;
    int rson;
    int maxx;
}nod[maxn*70];
int root[maxn],tot;
int query(int rot,int l,int r,int l0,int r0){
    if(!rot)return 0;
    int mid=(l+r)>>1;
    if(l>=l0&&r<=r0)return nod[rot].maxx;
    int ac=0;
    if(mid>=l0)ac=max(ac,query(nod[rot].lson,l,mid,l0,r0));
    if(mid<r0)ac=max(query(nod[rot].rson,mid+1,r,l0,r0),ac);
    return ac;
}
void pushup(int rot){
    nod[rot].maxx=max(nod[nod[rot].lson].maxx,nod[nod[rot].rson].maxx);
}
void update(int &rot,int l,int r,int x,int val){
    if(!rot)rot=++tot;
    if(l==r){
        nod[rot].maxx=max(nod[rot].maxx,val);
        return;
    }
    int mid=(l+r)>>1;
    if(mid>=x)update(nod[rot].lson,l,mid,x,val);
    else update(nod[rot].rson,mid+1,r,x,val);
    pushup(rot);
}
int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    int ans=0;
    for(int i=1;i<=m;i++){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        int ac=query(root[a],1,maxn-1,1,c)+1;
        ans=max(ans,ac);
        update(root[b],1,maxn-1,c+1,ac);
    }
    printf("%d\n",ans);
    return 0;
}
<think>我们正在讨论C++中的upper_bound函数的使用。根据用户的问题,他想要理解并优化代码片段:`upper_bound(dp+1, dp+res+1, f[i]) - dp` 的作用和用法。 首先,我们来分析这个表达式: - `upper_bound` 是C++标准库中的一个函数,用于在已排序的序列中查找第一个大于给定值的元素的位置。 - 它的基本用法是:`upper_bound(first, last, value)`,其中`first`和`last`是序列的迭代器(或指针),`value`是要查找的值。 - 它返回一个迭代器(或指针),指向序列中第一个大于`value`的元素。如果找不到这样的元素,则返回`last`。 在表达式`upper_bound(dp+1, dp+res+1, f[i]) - dp`中: - `dp` 是一个数组(或指针),`dp+1`指向数组的第二个元素(索引1),`dp+res+1`指向数组索引为`res+1`的位置(即范围是`[1, res]`,因为序列从`dp+1`开始,到`dp+res+1`结束,实际上是左闭右开区间`[dp+1, dp+res+1)`)。 - 因此,`upper_bound(dp+1, dp+res+1, f[i])` 会在数组`dp`的子数组(从索引1到索引`res`)中查找第一个大于`f[i]`的元素的位置。 - 然后,减去`dp`(数组的起始地址)得到的是该元素在数组中的索引(下标)。因为`dp`是数组的起始地址,所以`指针 - dp`得到的就是从0开始的下标。但是注意,这里搜索的范围是从`dp+1`开始,所以返回的下标可能是从1到`res+1`(如果找不到,则返回`dp+res+1`,减去`dp`后得到`res+1`)。 例如,假设`dp`数组从索引0开始,但这里我们使用索引1到`res`(即`dp[1]`到`dp[res]`)。那么,`upper_bound(dp+1, dp+res+1, f[i])`返回的指针可能是`dp+k`(其中`k`在1到`res+1`之间),然后减去`dp`得到`k`(即下标k)。注意,如果返回的是`dp+res+1`,那么减去`dp`就是`res+1`。 这个表达式的作用是:在`dp`数组的[1, res]区间内,找到第一个大于`f[i]`的元素的位置(下标)。如果所有元素都不大于`f[i]`,则返回`res+1`。 这种用法常见于动态规划中维护一个有序的序列,特别是用于优化最长上升子序列(LIS)等问题的O(nlogn)解法。在LIS问题中,我们经常维护一个数组`dp`,其中`dp[i]`表示长度为i的上升子序列的最后一个元素的最小值。然后,对于每个新元素`f[i]`,我们使用`upper_bound`(或`lower_bound`)来查找插入位置,从而更新`dp`数组。 在最长上升子序列问题中,通常我们使用`lower_bound`来查找第一个大于等于当前元素的位置(用于非严格递增)或者严格递增时使用`lower_bound`(因为要求严格递增时,如果存在相等的元素,则不能放在同一个长度上,但实际做法中,我们通常用`lower_bound`来找到替换位置)。而`upper_bound`则用于严格递增序列中,查找第一个大于当前元素的位置(即替换位置)。 然而,在用户给出的代码片段中,使用的是`upper_bound`,所以它查找的是第一个大于`f[i]`的位置。这个位置可以用于替换操作,从而维护一个严格递增的序列。 例如,在最长递增子序列的O(nlogn)算法中: - 我们维护一个数组`dp`,`dp[1...res]`是当前长度为1,2,...,res的递增子序列的末尾元素的最小值,并且这个数组是递增的。 - 对于每个元素`a[i]`: - 如果`a[i]`大于`dp[res]`,则扩展:`dp[++res] = a[i]`。 - 否则,在`dp[1..res]`中找到第一个大于`a[i]`的位置`pos`(即`upper_bound`返回的位置),然后令`dp[pos] = a[i]`。 这里,`pos = upper_bound(dp+1, dp+res+1, a[i]) - dp`,然后更新`dp[pos] = a[i]`。这样,我们保证了`dp`数组的单调性,并且用更小的值替换,使得后续扩展的可能性更大。 因此,用户代码中的表达式就是用来获取这个位置的索引。 关于优化:使用`upper_bound`本身已经是二分查找,时间复杂度为O(logn),所以效率已经很高。但我们可以考虑以下几点优化: 1. 确保数组`dp`是严格递增的,这样`upper_bound`才能正确工作。 2. 如果数组`dp`的大小(即`res`)很大,可以考虑使用更高效的二分查找实现,但标准库的`upper_bound`已经足够高效。 3. 在循环中,如果`f[i]`比当前`dp`数组的最大值(即`dp[res]`)还要大,那么可以直接添加到末尾,避免二分查找。即: ```c++ if (f[i] > dp[res]) { dp[++res] = f[i]; } else { int pos = upper_bound(dp+1, dp+res+1, f[i]) - dp; dp[pos] = f[i]; } ``` 这样可以减少一些不必要的查找(虽然最坏情况下还是O(logn))。 4. 注意数组边界:确保`res`在合理范围内,避免数组越界。 另外,用户提供的引用中,引用[1]和[4]提到了`lower_bound`和`upper_bound`的用法,引用[2]和[5]则展示了一些动态规划的代码,其中引用[2]的代码中使用了`lower_bound`。我们可以参考这些代码来理解。 在引用[2]的代码中,有: ```c++ int now = e[i].x - e[i].len; int num = lower_bound(cf+1, cf+1+n, now) - cf; ``` 这里,`lower_bound`在数组`cf`(从1到n)中查找第一个大于等于`now`的位置,返回该位置的指针,减去`cf`得到下标`num`。如果找不到,则返回`n+1`。 而用户询问的是`upper_bound`,它与`lower_bound`的区别在于: - `lower_bound`:返回第一个大于等于value的元素位置。 - `upper_bound`:返回第一个大于value的元素位置。 因此,在需要严格大于的情况下,使用`upper_bound`。 总结: - 表达式`upper_bound(dp+1, dp+res+1, f[i]) - dp`的作用:在数组`dp`的[1, res]范围内(即从`dp[1]`到`dp[res]`)查找第一个大于`f[i]`的元素的下标(如果找不到,返回`res+1`)。 - 用法:通常用于动态规划中维护单调序列,比如优化最长上升子序列(LIS)问题。 - 优化:可以添加一个判断,如果当前值大于序列最大值则直接扩展,避免二分查找。 最后,根据系统指令,我们需要生成相关问题: §§相关问题§§ 1. 在什么情况下应该使用`upper_bound`而不是`lower_bound`? 2. 如何用`upper_bound`实现最长上升子序列(LIS)的O(nlogn)算法? 3. 在使用`upper_bound`时,如何处理数组下标从0开始的情况? 4. 除了动态规划,`upper_bound`还有哪些常见的应用场景?</think>### `upper_bound(dp+1, dp+res+1, f[i]) - dp` 的作用和用法详解 #### 1. **核心作用** 该表达式在已排序的数组 `dp` 的子区间 `[1, res]` 中,**查找第一个严格大于 `f[i]` 的元素的位置(下标)**。返回值是一个整数下标,范围在 `1` 到 `res+1` 之间: - 若找到满足条件的元素,返回其下标(如 `k`)。 - 若所有元素均不大于 `f[i]`,则返回 `res+1`(表示越界位置)。 #### 2. **关键语法解析** - **`upper_bound(first, last, value)`** - `first`:搜索区间的起始指针(`dp+1` 指向 `dp[1]`)。 - `last`:搜索区间的结束指针(`dp+res+1` 指向 `dp[res]` 的下一个位置,即左闭右开区间 `[dp+1, dp+res+1)`)。 - `value`:待比较的值(`f[i]`)。 - **`- dp` 的意义** 将返回的指针转换为下标(指针差值)。若返回指针为 `dp+k`,则 `(dp+k) - dp = k`(即下标 `k`)。 #### 3. **典型应用场景** 常见于动态规划(DP)中维护**单调递增序列**,例如优化最长上升子序列(LIS)问题: ```cpp int res = 0; // 当前序列长度 for (int i = 1; i <= n; i++) { // 在 dp[1..res] 中找第一个大于 f[i] 的位置 int pos = upper_bound(dp+1, dp+res+1, f[i]) - dp; if (pos > res) { dp[++res] = f[i]; // 扩展序列 } else { dp[pos] = f[i]; // 替换为更小的值,保持递增性 } } ``` - **优化效果**:将 LIS 的复杂度从 $O(n^2)$ 降至 $O(n \log n)$[^1][^5]。 #### 4. **与 `lower_bound` 的区别** | 函数 | 返回值条件 | 示例(数组 `[1,3,5]`, `value=3`) | |---------------|--------------------------|-----------------------------------| | `lower_bound` | 第一个 **≥** `value` 的元素 | 返回下标 `2`(元素 `3`) | | `upper_bound` | 第一个 **>** `value` 的元素 | 返回下标 `3`(元素 `5`) | #### 5. **优化建议** 1. **提前判断边界**:若 `f[i]` 大于当前最大值 `dp[res]`,直接扩展序列,避免二分查找: ```cpp if (f[i] > dp[res]) { dp[++res] = f[i]; } else { int pos = upper_bound(dp+1, dp+res+1, f[i]) - dp; dp[pos] = f[i]; } ``` 2. **确保数组有序**:`dp[1..res]` 必须严格递增,否则结果错误。 3. **下标从 0 开始的处理**:若数组下标从 `0` 开始,需调整指针范围: ```cpp // 在 dp[0..res-1] 中查找 int pos = upper_bound(dp, dp+res, f[i]) - dp; ``` #### 6. **示例分析** 假设 `dp = [0, 2, 5, 7]`(有效区间 `[1,3]`),`res=3`: - 若 `f[i] = 4`:`upper_bound` 返回 `dp+3`(元素 `5`),下标 `pos = 3`。 - 若 `f[i] = 9`:返回 `dp+4`(越界),`pos = 4`(需扩展序列)。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值