题意
给定一个数轴上的 n 个区间,要求在数轴上选取最少的点使得第 i 个区间 [ai, bi] 里至少有 ci 个点
Input
输入第一行一个整数 n 表示区间的个数,接下来的 n 行,每一行两个用空格隔开的整数 a,b 表示区间的左右端点。1 <= n <= 50000, 0 <= ai <= bi <= 50000 并且 1 <= ci <= bi - ai+1。
Output
输出一个整数表示最少选取的点的个数
输入样例
5
3 7 3
8 10 3
6 8 1
1 3 1
10 11 1
输出样例
6
提示
分析
这道题曾经做过,经典的解题方法是贪心算法👉[week3]区间选点问题——贪心算法。不过这次用的是一种完全不一样的做法——差分约束系统
- 差分约束系统
- 什么是差分约束系统
差分约束系统可能在最开始有点难以理解,不过仔细想一下还是可以明白的。
差分约束是指一组形如以下的方程组:
x1 - x0 <=c1
x2 - x1 <= c2
x3 - x2 <= c3
…
(ck为常数,可正可负)
显然这组方程组可能存在多种解,使得方程组中所有的方程(约束条件)满足。若不存在这样的解,说明该差分约束无解。
而我们要做的就是试图找出其中的一组解。
- 如何求解差分约束系统
- 一般思路
将所有相关联的方程联系起来得到一些式子来进行求解。
比如上述三个方程式在组合之后就可以得到关于x3和x0的关系式。
- 路径法
这是一种很重要也很巧妙的思想。
将上述提到的差分约束式子进行转换后可以得到:
xi <= xj + ck
而这和求单源最短路径时的松弛操作中所出现的松弛比较非常相似:
if ( xi > xj + ck )
xi = xj + ck;
不难想象,该松弛操作反复多次后最终得到的xi一定满足:
xi <= xj + ck
显然,这就是差分约束。所得到的单源最短路径一定是可以满足差分约束的一组解。
所以,通过这样的转换联系,我们可以把求解差分约束的一组解转换为求最短路径的问题。
💡理一下思路:
也就是,形如差分约束:
xi - xj <= ck
可以首先转换为:
xi <= xj + ck
而以上的这个式子可以通过松弛操作得到:
if ( xi > xj + ck )
xi = xj + ck;
因此,该式子中的三个元素:xi xj ck 可以转换为图中的边:
xi <= xj + ck
——> xi <= xj + w(j,i)
——> 一条由j连向i的边,权重为ck
求差分约束式子的解就可以转换为求最短路径:
求xi <= xj + ck
的解
——>求xi到起点的最短路径(单源最短路径问题)
- 实现方法
显然,在这个由差分约束转换的图结构中存在负权,因此需要用SPFA来解决。
SPFA👉[week7]TT的美梦——SPFA
在这里要讨论关键的几个问题:
负权环路和不可达在差分约束系统中的意义?
在图结构中,这两种情况都代表着从某一固定点到出现该情况的点不存在最短路径。那么,转换到差分约束当中就能发现,这代表着方程组中的一些方程式不成立。因此,这就代表着当前差分约束系统无解。
- 最大解和最小解
这是一个容易混乱的问题,需要着重理解。
SPFA不仅可以用于求单源最短路径,还可以求单源最长路径。
而这两种求法得到的解在差分约束系统中都是存在的,且这两种解分别代表着差分约束系统的最大解和最小解。
那么如何来理解呢?
- 最短路和最大解
if ( xi > xj + ck )
xi = xj + ck;
这是我们常见的,也是SPFA中用于求最短路径时的松弛操作。经过之前的分析,已经基本明白松弛最后所得到的结果一定满足:
xi <= xj + ck
但是,思考一下就能发现,在实现过程中,一旦出现:
xi = xj + ck
松弛就不会再继续。
也就是说,松弛得到的结果将最终停留在:
xi = xj + ck
但是差分约束系统中规定的却是“ <= ”,这就代表着xi还可以更小。所以,最短路径求法所得到的,是满足差分约束系统的最大解。
- 最长路和最小解
最长路的求法不常见,我们一般只讨论最短路。但实际上通过SPFA求单源最长路径时,只需要在最短路径的松弛比较上进行修改就可以。
if ( xi < xj + ck )
xi = xj + ck;
为什么这么修改应该很好理解。同样的,通过最短路径和差分约束系统分析进行举一反三,我们可以发现这个松弛比较所得到的差分约束条件是:
xi >= xj + ck
——>xi - xj >= ck
也许此时你会疑惑,差分约束不是" <= “吗?为什么这里变成了” >= "?
其实对这个式子稍微做一点变换就会变成最开始见到的模样了。可以出现这种式子的主要原因是ck可以取负数,自然也能转换为" >= "。
同样的,在求解过程中,当出现:
xi = xj + ck
松弛就不会再继续。
也就是说,松弛得到的结果将最终停留在:
xi = xj + ck
但是差分约束系统中规定的是“ >= ”,这就代表着xi还可以更大。所以,最长路径求法所得到的,是满足差分约束系统的最小解。
在求解最小解的情况下,我们就不需要将这样的" >= “再转换为最初形式,而是可以直接用其进行边的转换。但是如果我们求解的是最大解,而从题目中所得到的却是” >= "时,就要将其转换为原来的形式啦。
如果有时候突然忘了在不同情况下边如何转换(突然忘记边的方向),可以把差分约束式子转换为对应求解方法下的松弛比较式子,再进行思考,就很容易啦。
- 转换为差分约束系统
很多题目给出的约束条件并不像差分约束,但实际上可以试着进行转换,如果能变成差分约束,那就代表着这道题或许可以用这样的方法来解决。
除了这些基础不等式的转换为,还有一个就是:
xi / xj <= k
这样的除法式子也可以通过两边取对数来实现转换。
【小tip:但是要注意的是,一般的题目条件转换为差分约束条件时,除了显式的数据关系外,一般都会有隐藏的数据约束。比如相邻数据之间的关系等。】
- 求路径时起点如何选择?
这要看具体的题目要求,显然大多数情况下都是以0为起点。
- 题目分析
题目所要求的就是在每个给定区间中都至少包含对应取点个数,并且保证最后整体取点数最少。
这要如何转换为差分约束系统呢?
若将sum[i]视作区间[0,i]中所取点数,那么容易得知:
sum[b] - sum[a - 1]
就是区间[a,b]中的取点数。
因此,题目条件“区间[a,b]中的取点数至少为c”就可以转换为:
sum[b] - sum[a - 1] >= c
而求出最少的点数,显然这是求最小解。
因此,这道题就可以转换为求差分约束系统的最小解问题,再转换为图论单源最长路径问题。
那么最终区间内的取点数自然就是所给出所有区间中最右端点到0之间的取点数了,也就是最大点到起点0的最长路径。
不过,需要注意的是,在相邻两个端点之间存在着隐式约束关系:
sum[i-1] >= sum[i] - 1
sum[i] >= sum[i-1] + 0
- 优化&问题
1.实际在求解路径过程中需要遍历的点
虽然求解的点数是散步在0到最右端点之间,但是如果题目给出有约束的区间最左端点与0有一段距离,那显然这段空白区间我们不需要再进行遍历。
因此可以在创建边的过程中,不仅记录最右端点,同时也记录最左端点。所有的实际操作都只针对这两个端点之内的区间。
同时,这也代表着,这0到最左端点之间(不含左端点)一定不包含任何点,所以SPFA的起点就可以改成最左端点。
2. 0端点
这个问题让我debug了半天,一直超时👋
我从一开始就意识到了0端点的特殊性。
根据推倒的差分约束式子:
sum[b] - sum[a - 1] >= c
可以转换为:
sum[b] >= sum[a - 1] + c
我们可以知道,在图中的边应该为:
(a - 1) —> b 权重为c
但是显然,当a为1时,点序号不合法。
最开始我是打算通过单独考虑这个情况来,但是因为各种稀奇古怪的小问题,最后我就不耐烦到放弃,直接将所有索引+1。当所有索引同步右移时,虽然端点变了,但答案实质并没有改变。显然,这种处理方式也更方便。
3. {}实现结构体和pair类型的赋值及插入
在提交a题的时候就出现了所有编译器都ce的情况。问题就出现在向结构体类型数组中直接用{}插入。这是c++11才能支持的操作,如果选用了这种表示法,一定要记得匹配c++11!除此之外,在c++11以外的编译器中,结构体和pair类型只有初始化的时候可以用{}赋值,其他时候都不能用{}直接给一个变量赋值!
总结
- 在写代码的过程中不断理解差分约束系统的转换还是很有意思的😌
- 但是小细节半天无法AC真的很让人暴走🤯
代码
//
// main.cpp
// lab1
//
//
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#define _CRT_SECURE_NO_WARNING
using namespace std;
struct edge
{
int start,end,num,next; //存储一个区间[start,end]中至少要选出的点数num
};
int sum[50100]; //代表区间[0,i]中选择点的个数
//vector<edge> interval[60000]; //存储所有不等式对应边
queue<int> q;
bool vis[50100]; //标记当前点是否已在队列中
edge interval[10000000];
int head[50100];
int tot;
void ini(int n)
{
tot = 0;
for( int i = 0 ; i < n ; i++ )
head[i] = -1;
}
void add(int s,int e,int w)
{
interval[tot].start = s;
interval[tot].end = e;
interval[tot].num = w;
interval[tot].next = head[s];
head[s] = tot;
tot++;
}
void spfa(int s) //最长路spfa,实际上是找到最小解
{
q.push(s);
while( !q.empty() )
{
int now = q.front();
q.pop();
vis[now] = 0;
// cout<<" ============ "<<interval[now].size()<<endl;
// for( int i = 0 ; i < interval[now].size() ; i++ ) //对当前点的邻接点松弛
for( int i = head[now] ; i != -1 ; i = interval[i].next )
{
int to = interval[i].end,num = interval[i].num;
// int to = interval[now][i].end;
// cout<<now<<" now "<<to<<" to "<<endl;
if( sum[to] < sum[now] + num ) //若松弛成功
{
sum[to] = sum[now] + num; //更新答案
// cout<<sum[to]<<" sum[to] "<<endl;
if( !vis[to] ) //若当前点已经在队列中,就不需要重复入队了
{
q.push(to);
vis[to] = 1;
// cout<<to<<" ! "<<endl;
}
}
}
}
}
int main()
{
ios::sync_with_stdio(false);
int n = 0,a = 0,b = 0,c = 0,maxs = 0,min = 100000;
// cin>>n;
scanf("%d",&n);
ini(50000);
for( int i = 0 ; i < n ; i++ ) //存入该区间信息
{
// cin>>a>>b>>c;
scanf("%d %d %d",&a,&b,&c); //c++11才能支持直接将输入内容pushback
a++; //为了避免遇到边界值0
b++;
add(a - 1, b, c);
if( b > maxs ) //记录最右端点
maxs = b;
if( a - 1 < min ) //记录最左端点
min = a - 1;
}
//最小从1开始进行合法边插入
for( int i = min ; i <= maxs ; i++ )
{
add(i, i - 1, -1); //sum[i-1]>=sum[i]-1
add(i - 1, i, 0); //sum[i]>=sum[i-1]+0
// edge d = {i,i - 1,-1};
// interval[i].push_back(d);
// edge e = {i - 1,i,0}; //绝了,必须重新声明一个edge,直接用d等于{}也不行
// interval[i - 1].push_back(e);
sum[i] = -1; //初始化点数数组
vis[i] = 0;
}
sum[min] = 0;
spfa(min);
// cout<<sum[max]<<endl;
printf("%d\n",sum[maxs]);
return 0;
}