洛谷题单指南-分治与倍增-P7562 [JOISC 2021 Day4] イベント巡り 2 (Event Hopping 2)
原题链接:https://www.luogu.com.cn/problem/P7562
题意解读:每个活动都是一个区间,在n个区间里选k个没有交叉或者包含关系的区间,使得k个区间编号的字典序最小。
解题思路:
此题本质上是一个区间选择问题,遍历每一个区间,如果这个区间能选就一定优先选先该区间,这样能保证字典序最小。
因此,要解决的关键问题就是:如何判断一个区间能不能选,展开来主要有两点:
1、判断一个区间与之前已经选择的区间是否有冲突(交叉或者包含)
2、判断选择一个区间之后,剩下的区间可选数量能确保一共够k个
第一个问题:
可以借助set来对区间交叉以及包含的情况进行去重
定义区间结构体range以及set<range>
struct range
{
int l, r;
bool operator < (const range &x) const
{
return r < x.l;
}
};
set<range> tr;
这样,当往set中insert区间range时,如果有交叉或者包含关系的range,会被视为相等从而起到去重的效果。
具体解释下为什么有交叉或包含关系的range在set中被视为相等:
设两个区间x, y,
交叉时:x.r > y.l , y.r > x.l,不满足operator <定义的顺序比较,因此视为相等。
包含时:x.r > y.l, y.r > x.l,,不满足operator <定义的顺序比较,因此视为相等。
本题中,定义set<range> tr表示当前所有可用的空闲区间范围,一旦选择一个可用区间[x,y],需要在tr中查找到完全包含[x,y]的区间[l,r]
然后在tr中删除[l,r]区间,添加[l,x],[y,r]两个区间,这样就完成了区间的分割操作。
在set中查找是否存在包含一个区间可以用find,之后再进一步判断找到的区间是否完全包含而不是交叉。
第二个问题:
query(x, y)函数表示在[x,y]区间中可选区间最大数量
设初始区间是[start, end], 初始可选区间数量为all = query(start, end)
再进行若干次选择之后all会持续更新,更新方式如下:
当前选择了一个可用区间[x, y],在tr中找到的包含区间为[l,r],则剩余可选区间的数量变为all - query(l, r) + query(l, x) + query(y, r)
判断剩余可选区间数量 + 已选区间数量是否 >= k即可
因此,关键在于如何计算query(x, y),也就是[x, y]范围内可以最多选多少个区间?
贪心做法:
选一个当前区间[x, y],提前对所有区间按左端点排好序,下一个区间是枚举所有左端点大于等于y的区间,取一个右端点最小的,累计区间数量。
此方法,总的时间复杂度超过n^2,会超时。
32分代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
struct range
{
int l, r;
bool operator < (const range &x) const
{
return r < x.l;
}
};
range a[N], b[N];
set<range> tr;
vector<int> ans;
int all; //剩余可选最大区间
int start = INT_MAX, ed = INT_MIN;
int n, k;
bool cmp(range x, range y)
{
return x.l <= y.l;
}
int query(int l, int r)
{
int res = 0, lastr = l;
for(int i = 1; i <= n && b[i].l <= r; i++)
{
bool find = false;
int minr = INT_MAX;
for(int j = i; j <= n && b[j].l <= r; j++)
{
if(b[j].l >= lastr && b[j].r <= r)
{
find = true;
if(b[j].r < minr)
{
minr = b[j].r;
i = j;
}
}
}
lastr = minr;
if(find) res++;
}
return res;
}
int main()
{
cin >> n >> k;
for(int i = 1; i <= n; i++)
{
cin >> a[i].l >> a[i].r;
start = min(start, a[i].l);
ed = max(ed, a[i].r);
}
memcpy(b, a, sizeof(a));
sort(b + 1, b + n + 1, cmp); //b按左端点排序
tr.insert({start, ed}); //初始空闲区间范围
all = query(start, ed);
for(int i = 1; i <= n; i++)
{
range cur = a[i]; //当前区间
int x = cur.l, y = cur.r;
auto it = tr.lower_bound(cur);
if(it != tr.end())
{
int l = (*it).l, r = (*it).r;
if(l <= x && r >= y) //找到包含cur的区间
{
if(all - query(l, r) + query(l, x) + query(y, r) >= k - ans.size() - 1)
{
all = all - query(l, r) + query(l, x) + query(y, r);
ans.push_back(i);
tr.erase(it);
tr.insert({l, x});
tr.insert({y, r});
}
}
if(ans.size() == k)
{
for(auto v : ans) cout << v << endl;
return 0;
}
}
}
cout << -1;
return 0;
}
倍增和ST表做法:
既然贪心枚举不行,需要优化,思考倍增方式,
设st[i][j]表示从i坐标开始,选择了2^j个区间之后,所能达到的最左的位置。
那么要计算query(x, y),
可以从大到小枚举j: log2(n) ~ 0,cur = x,如果st[cur][j] <= y,累加2^j,cur = st[cur][j]。
如何计算st[i][j]?
由于坐标取值范围达到10^9,首先需要对坐标进行离散化处理。
然后,借助st表的模型可以得到st[i][j] = st[st[i][j - 1]][j - 1]
初始时,
由于从所有区间左端点开始选择1个区间,一定到达其右端点,所以有
对于所有区间的[l, r],st[l][0] = min(st[l][0], r)
但是,除了区间的左端点,其他坐标如何初始化?
对于每一个坐标i都可以用右边一个坐标来更新st[i][0] = min(st[i][0], st[i+1][0]),注意i要从大到小遍历。
100分代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
struct range
{
int l, r;
bool operator < (const range &x) const
{
return r < x.l;
}
};
range a[N]; //所有活动
vector<int> c; //所有起始、结束时间
map<int, int> d; //离散化所用map
set<range> tr; //所有空闲的区间
int st[2 * N][20];
vector<int> ans;
int all; //剩余可选最大区间
int n, k;
void init()
{
memset(st, 0x3f, sizeof(st));
for(int i = 1; i <= n; i++)
{
st[a[i].l][0] = min(st[a[i].l][0], a[i].r);
}
for(int i = c.size() - 1; i >= 0; i--)
{
st[i][0] = min(st[i][0], st[i + 1][0]);
}
for(int j = 1; j <= log2(n); j++)
{
for(int i = 0; i < c.size(); i++)
{
if(st[i][j - 1] < c.size())
st[i][j] = st[st[i][j - 1]][j - 1];
}
}
}
int query(int l, int r)
{
int res = 0, cur = l;
for(int j = log2(n); j >= 0; j--)
{
if(st[cur][j] <= r)
{
res += (1 << j);
cur = st[cur][j];
}
}
return res;
}
int main()
{
cin >> n >> k;
for(int i = 1; i <= n; i++)
{
cin >> a[i].l >> a[i].r;
c.push_back(a[i].l);
c.push_back(a[i].r);
}
//离散化
sort(c.begin(), c.end());
c.erase(unique(c.begin(), c.end()), c.end());
for(int i = 0; i < c.size(); i++) d[c[i]] = i;
for(int i = 1; i <= n; i++) a[i].l = d[a[i].l], a[i].r = d[a[i].r];
init(); //初始化st表
tr.insert({0, (int)c.size() - 1}); //初始空闲区间范围
all = query(0, c.size() - 1); //剩余可选区间数
for(int i = 1; i <= n; i++) //枚举所有区间
{
range cur = a[i]; //当前区间
int x = cur.l, y = cur.r;
auto it = tr.find(cur);
if(it != tr.end())
{
int l = it->l, r = it->r;
if(l <= x && r >= y) //找到包含cur的区间
{
if(all - query(l, r) + query(l, x) + query(y, r) >= k - ans.size() - 1) //选i区间后剩下的区间够满足k个
{
//选择第i个区间
all = all - query(l, r) + query(l, x) + query(y, r);
ans.push_back(i);
tr.erase(it);
tr.insert({l, x});
tr.insert({y, r});
}
}
if(ans.size() == k)
{
for(auto v : ans) cout << v << endl;
return 0;
}
}
}
cout << -1;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?