前置文章: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
成员变量的类)在使用上有以下异同点:
一、相同点
-
存储数据
两者均可用于存储两个整数,并通过成员变量(如.first
、.second
)直接访问数据。 -
作为函数返回值
均适合用于函数返回多个值的场景。例如:// 使用 pair std::pair<int, int> getCoordinates() { return {x, y}; } // 使用自定义类 struct Point { int x; int y; }; Point getCoordinates() { return {x, y}; }
-
支持赋值和拷贝
两者都支持直接赋值和拷贝操作: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_size
和tuple_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容器和算法无缝兼容。例如
-
自定义类
- 若要在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); } }; }
- 若要在STL容器中使用,可能需要额外定义哈希函数或比较器:
-
std::pair
- 语法简洁,适合快速组合两个值,减少代码冗余。
- 例如:
return {a, b};
直接返回一个pair
。
-
自定义类
- 需要更多代码定义类及其方法,但在复杂场景中更清晰。
三、适用场景建议
-
使用
std::pair
的场景- 临时存储两个值(如函数返回值)。
- 需要与STL容器(如
map
、unordered_map
)直接交互。 - 不需要额外方法或复杂逻辑的简单数据组合。
-
使用自定义类的场景
- 数据需要附加行为(如打印、计算)。
- 需要扩展性(如添加第三个成员变量)。
- 需要更清晰的语义(例如用
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;
}