区间问题大纲(前四种是贪心)

 前置文章:DP vs 贪心:石子合并 与 合并果子

目录

一、区间选点

二、最大不相交区间数量

三、区间分组

四、区间覆盖

五、区间合并

 六、区间和(保序离散化)


本篇前四个区间问题均为贪心

一、区间选点

​​​​​该问题是“单峰函数”每次选择局部最优解即可找到全局最优解

 

 ①可证明选出来的区间无交集、且数量为最大值

②从小于总数量角度易证

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

const int N = 100010;
int n;
struct Range//运算符重载不一定非要在class中定义对象用
{
    int l, r;
    bool operator< (const Range &w)const//常量化、引用 Range类型的w,
    {//最后的const表示该函数不会修改类的成员变量(如果是类的成员函数的话)。
        return r < w.r;//为sort函数准备排序依据
    }
}range[N];

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        cin>>l>>r;
        range[i] = {l, r};//range的元素是个结构体,和动态规划的区别也在此
    }

    sort(range, range + n);//排序数组下标0到下标n-1(包括第n-1个)元素,根据重载的<运算符定义来比较struct对象间大小

    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i ++ )
        if (range[i].l > ed)//如果 当前区间左端点 比 上一个最大的区间右端 大的话那么就需要新选择点res++
        {
            res ++ ;
            ed = range[i].r;//当前区间右端点重新设置为最大区间范围右端点(变为 上一个最大的区间右端点)
        }

    cout<<res;

    return 0;
}

二、最大不相交区间数量

 本题与上一题流程、代码基本相同

三、区间分组

 

①如上面三个线段分出来两组可证

②按照左端点排序,遍历到L[i]区间,前i-1区间左端点都是小于L[i]左端点的,假设前i-1区间都有重叠部分也就是此时cnt至少为i,且L[i].l<组.r_max那么往后肯定ans>=cnt

用堆来动态维护每组r最小值

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

using namespace std;

const int N=100010;
int n;

struct Range
{
    int l, r;
    bool operator<(const Range &w) const
    {
        return l < w.l;
    }
}range[N];

int main()
{
    cin>>n;
    
    for (int i = 0; i < n; i++)
    {
        int l, r;
        cin>>l>>r;
        range[i] = {l, r};
    }

    sort(range, range + n);

    priority_queue<int, vector<int>, greater<int>> heap;

    for (int i = 0; i < n; i++)
    {
        auto r = range[i];
        if (heap.empty() || heap.top() >= r.l)
            heap.push(r.r);
        else
        {
            int t = heap.top();
            heap.pop();
            heap.push(r.r);
        }
    }

    cout<<heap.size();

    return 0;
}

按左端点排序还是按右端点排序目前没有一个很好的总结,这也是贪心问题的难点。

四、区间覆盖

 

 1.所有区间按左端点从小到大排序

 2.从前往后依次枚举每个区间,在所有能覆盖start的区间中,选择右端点最大的区间,让后将start更新为该右端点作为右端点最大值。

ans是最优解,假设cnt是算法没找到的情形反证,得到算法一定不会算出cnt比如上图应该为4个区间却有5个这种情况。因为算法过程会进行自身调整。

本题代码要稍微注意一下内部流程的处理技巧

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return l < W.l;
    }
}range[N];

int main()
{
    int st, ed;//指定区间的start end
    cin>>st>>ed;
    cin>>n;
    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        cin>>l>>r;
        range[i] = {l, r};
    }

    sort(range, range + n);

    int res = 0;
    bool success = false;
    for (int i = 0; i < n; i ++ )//i从第一个区间向后枚举
    {
        int j = i, r = -2e9;//j从上一轮最后一个区间向后枚举
        while (j < n && range[j].l <= st)
        {
            r = max(r, range[j].r);//过程记录右端点修改为“较大值”
            j ++ ;
        }

        if (r < st)//本轮右端点比上一轮的右端点要小,那没结果
        {
            res = -1;
            break;
        }

        res ++ ;
        if (r >= ed)
        {
            success = true;
            break;
        }

        st = r;//本轮作为相对下一轮来讲的上一轮,右端点st用本轮的右端点替代。
        i = j - 1;
    }

    if (!success) res = -1;
    cout<<res;

    return 0;
}

五、区间合并

核心是用st、ed来维护当前遍历到的区间

 ①按照区间左端点排序

②依次和后续区间对比,

如果ed大于遍历到的L的右端点那么就st、ed不变,

如果说L的左端点比ed小那么将ed变为L的右端点(取并集),

如果说后续的某一个区间L的左端点比ed要大那么st和ed变为L的左右端点,总数量++再后续进行以上操作

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

int n;
typedef pair<int,int> PII;
vector<PII> segs;

void merge(vector<PII> &segs)
{
    
    vector<PII> res;//使用res来保存更新过后的区间
    
    sort(segs.begin(),segs.end());//默认按照pair的first进行排序,如需要按照second进行排序则需要lambda表达式
    int st=-2e9,ed=-2e9;
    for(auto seg:segs)
    {
        if(ed<seg.first)//取st~ed作为“标杆”,如果后续遍历到的区间L的左端点比标杆右端点ed大则最后区间总数结果应该++
        {
            if(st!=-2e9)res.push_back({st,ed});//这里不仅把总数++,还把区间有哪些也表示进去了
            st=seg.first,ed=seg.second;//这边在最后一个区间的时候,无法把它push_back进res
            //所以在后第四行if(st!=-2e9)res.push_back({st,ed});的意思是将最后一个遍历到作为“标杆”的区间也加进去
        }
        else ed=max(ed,seg.second);
    }
    if(st!=-2e9)res.push_back({st,ed});
    segs=res;

}

int main()
{
    cin>>n;
    //先保存而后进行合并
    while(n--)
    {
        int l,r;
        cin>>l>>r;
        segs.push_back({l,r});
    }
    
    merge(segs);
    cout<<segs.size();
    // for(auto seg:segs)
    // {
    //     cout<<seg.first<<seg.second<<endl;
    // }
    
    return 0;
}

Tips:pair<int,int>与定义class中定义两个int类型成员变量在使用上的异同点:

在C++中,std::pair<int, int>和自定义类(如包含两个int成员变量的类)在使用上有以下异同点:


一、相同点

  1. 存储数据
    两者均可用于存储两个整数,并通过成员变量(如.first.second)直接访问数据。

  2. 作为函数返回值
    均适合用于函数返回多个值的场景。例如:

    // 使用 pair
    std::pair<int, int> getCoordinates() { return {x, y}; }
    
    // 使用自定义类
    struct Point { int x; int y; };
    Point getCoordinates() { return {x, y}; }

  3. 支持赋值和拷贝
    两者都支持直接赋值和拷贝操作:

    std::pair<int, int> p1 = {1, 2};
    std::pair<int, int> p2 = p1; // 拷贝
    
    Point pt1 = {3, 4};
    Point pt2 = pt1; // 拷贝


二、不同点

1. 语法和声明

  • std::pair
    直接使用模板类,无需额外定义:

    #include <utility>
    std::pair<int, int> p; // 直接声明

  • 自定义类
    需要显式定义类或结构体:

    struct MyPair {
        int first;
        int second;
    };
    MyPair mp; // 声明

2. 默认功能

  • std::pair

    • 自动生成比较运算符(如==<>等),前提是成员类型支持比较。
    • 支持结构化绑定(C++17起):
      auto [a, b] = std::pair{5, 10}; // a=5, b=10

  • 自定义类

    • 默认不生成比较运算符,需手动重载:
      bool operator==(const MyPair& lhs, const MyPair& rhs) {
          return lhs.first == rhs.first && lhs.second == rhs.second;
      }

    • 若需结构化绑定,需显式声明tuple_sizetuple_element(复杂且不常用)。

3. 扩展性

  • std::pair

    • 功能固定,无法添加成员函数或额外成员变量。
    • 适合简单数据存储,无额外逻辑。
  • 自定义类

    • 可自由添加方法、构造函数、析构函数等:
      struct Point {
          int x, y;
          void print() const { std::cout << x << ", " << y; }
      };

    • 适合需要封装数据行为的场景。

4. 与STL的兼容性

  • std::pair

    • 与STL容器和算法无缝兼容。例如std::map的键值对直接使用std::pair
      std::map<int, std::string> myMap;
      myMap.insert({1, "Apple"}); // {1, "Apple"} 是 pair<int, string>

  • 自定义类

    • 若要在STL容器中使用,可能需要额外定义哈希函数或比较器:
      struct MyKey {
          int id;
          std::string name;
      };
      
      // 自定义哈希函数
      namespace std {
          template<> 
          struct hash<MyKey> {
              size_t operator()(const MyKey& k) const {
                  return hash<int>()(k.id) ^ hash<string>()(k.name);
              }
          };
      }

  • std::pair

    • 语法简洁,适合快速组合两个值,减少代码冗余。
    • 例如:return {a, b}; 直接返回一个pair
  • 自定义类

    • 需要更多代码定义类及其方法,但在复杂场景中更清晰。

三、适用场景建议

  1. 使用std::pair的场景

    • 临时存储两个值(如函数返回值)。
    • 需要与STL容器(如mapunordered_map)直接交互。
    • 不需要额外方法或复杂逻辑的简单数据组合。
  2. 使用自定义类的场景

    • 数据需要附加行为(如打印、计算)。
    • 需要扩展性(如添加第三个成员变量)。
    • 需要更清晰的语义(例如用Point代替pair<int, int>表示坐标)。

四、示例对比

// 使用 pair
auto p = std::make_pair(10, 20);
std::cout << p.first << ", " << p.second; // 输出 10, 20

// 使用自定义类
struct Point {
    int x, y;
    void print() const { std::cout << x << ", " << y; }
};
Point pt{30, 40};
pt.print(); // 输出 30, 40


总结

  • std::pair:轻量、便捷,适合简单场景和STL交互。
  • 自定义类:灵活、可扩展,适合复杂逻辑和语义化需求。
    根据具体需求选择,两者互补而非替代。

 六、区间和(保序离散化)

 

 去重的必要性

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;


typedef pair<int,int> PII;
const int N=300010;//一个插入操作一个坐标、一次询问要两个坐标,n、m在10^5数量级,坐标总数需要3*10^5

int a[N],s[N];
//a数组中每个数据元素不是数轴上的地址而是虚拟的映射index,s数组就是前缀和

vector<int> alls;//存放的是集中存放的数轴地址,紧随其后集中存放每次询问的左右数轴地址

int n,m;
vector<PII> add,query;//题目的输入可以包括三个部分,n和m的输入,
//一块是  add操作  一块是  query操作  ,两个操作都是以对组、绑定、整体形式出现的


int find(int x)
{
    int l=0,r=alls.size()-1;
    while(l<r)
    {
        int mid=l+r>>1;
        if(alls[mid]>=x)r=mid;
        else l=mid+1;
    }
    return r+1;
}


//vector中unique函数的实现方式
// vector<int>::iterator unique(vector<int> &a)
// {
//     int j = 0;
//     for (int i = 0; i < a.size(); i++ )
//         if (!i || a[i] != a[i - 1])//所有满足这两个性质的数就是我们要找的不重复的数
            //要么它是第一个,要么它与它前一个数不一样
//             a[j++] = a[i];
//     // a[0] ~ a[j - 1] 所有a中不重复的数
//     return a.begin() + j;
// }


int main()
{
    cin>>n>>m;
    //预处理,先存再做
    for(int i=0;i<n;i++)
    {
        int x,c;
        cin>>x>>c;
        add.push_back({x,c});
        //不仅在add中保存对组,在alls的后面也要存一份地址
        alls.push_back(x);
    }
    
    for(int i=0;i<m;i++)
    {
        int l,r;//数轴地址
        cin>>l>>r;
        query.push_back({l,r});
        //不仅在query中保存,在alls的再后面也要存一份l、r
        alls.push_back(l);
        alls.push_back(r);
    }
    
    //去重
    sort(alls.begin(),alls.end());
    alls.erase(unique(alls.begin(),alls.end()),alls.end());
    
    // 处理插入
    for (auto item : add)//add操作 元素为对组  x位置加c
    {
        int x = find(item.first);//这个返回的x是数轴地址吗?不是,而是alls中的小范围的 映射下标
        a[x] += item.second;//second就是题干中的c。虽然上面做了去重,但是这里可以通过add找到某个重复位置加了几次,从而在映射小的数组中后续不断加上
    }

    // 预处理前缀和
    for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];//只需计算小的数据范围

    // 处理询问
    for (auto item : query)//query操作 元素为对组 从l位置到r位置
    {
        int l = find(item.first), r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }



    
    
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值