跳转至

POJ-3190 Stall Reservations(贪心,区间划分+路径输出)

Description

Oh those picky N (1 <= N <= 50,000) cows! They are so picky that each one will only be milked over some precise time interval A..B (1 <= A <= B <= 1,000,000), which includes both times A and B. Obviously, FJ must create a reservation system to determine which stall each cow can be assigned for her milking time. Of course, no cow will share such a private moment with other cows.

Help FJ by determining:

  • The minimum number of stalls required in the barn so that each cow can have her private milking period
  • An assignment of cows to these stalls over time

Many answers are correct for each test dataset; a program will grade your answer.

Input

Line 1: A single integer, N

Lines 2..N+1: Line i+1 describes cow i's milking interval with two space-separated integers.

Output

Line 1: The minimum number of stalls the barn must have.

Lines 2..N+1: Line i+1 describes the stall to which cow i will be assigned for her milking period.

Sample Input

5
1 10
2 4
3 6
5 8
4 7

Sample Output

4
1
2
3
2
4

Hint

Explanation of the sample:

Here's a graphical schedule for this output:

Time     1  2  3  4  5  6  7  8  9 10

Stall 1 c1>>>>>>>>>>>>>>>>>>>>>>>>>>>

Stall 2 .. c2>>>>>> c4>>>>>>>>> .. ..

Stall 3 .. .. c3>>>>>>>>> .. .. .. ..

Stall 4 .. .. .. c5>>>>>>>>> .. .. ..

Other outputs using the same number of stalls are possible.


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

using namespace std;

const int INF = 0x0ffffff;

struct Node
{
    int left, right;
    int no; //记录输入的原始序号
    //左端点升序,右端点降序
    bool operator<(const Node & n) const
    {
        return (left < n.left || (left == n.left && right < n.right));
    }
};

struct Stall
{
    int end;
    int pos;
    bool operator<(const Stall & s) const
    {
        return end > s.end;
    }
    Stall(int e, int p) : end(e), pos(p) {}
};

int n = 50001;

vector<Node> sequence(n);
vector<int> record(n); //记录每头牛被分配到stall的编号

int main()
{
    std::ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> sequence[i].left >> sequence[i].right;
        sequence[i].no = i;
    } 
    sort(sequence.begin() + 1, sequence.begin() + 1 + n);

    int cnt = 0;
    priority_queue<Stall> pq;

    for (int i = 1; i <= n; ++i) {
        if (pq.empty()) { //队列为空,还没有安排围栏
            ++cnt;
            pq.push(Stall(sequence[i].right, cnt));
            record[sequence[i].no] = cnt;
        }
        else { //队列不为空,存在两种情况
            Stall tmp = pq.top();
            if (tmp.end < sequence[i].left) { //当前围栏可以加入
                pq.pop(); //删除原来的元素,更新
                pq.push(Stall(sequence[i].right, tmp.pos));
                record[sequence[i].no] = tmp.pos;
            }
            else { //已经没有可以加入的位置了,需要增加围栏
                ++cnt;
                pq.push(Stall(sequence[i].right, cnt));
                record[sequence[i].no] = cnt;
            }
        }
    }

    cout << cnt << endl;
    for (int i = 1; i <= n; ++i) cout << record[i] << endl;

    return 0;
}

我们把问题抽象一下再描述:

时间限制: 1 Sec 内存限制: 128 MB 时间轴上有n个开区间(ai, bi),把这些区间至少划分成多少个集合,使得每个集合中的区间两两没有公共点。因为是开区间,所以(1, 2)和(2,3)可在一个集合中

输入

第1行:一个整数N(1 <= N <=10^5) 接下来N行,每行2个整数Ai,Bi(0<= Ai < Bi < 10^7)

输出

第1行:1个整数,需要划分成的最少集合数。

样例输入

6
4 7
2 5
1 2
3 5
3 6
7 9

样例输出

4

#include <iostream>
#include <cmath>
#include <vector>
#include <algorithm>

using namespace std;

const int INF = 0x0ffffff;

struct Node
{
    int left, right;
    //左端点升序,右端点降序
    bool operator<(const Node & n) const
    {
        return (left < n.left || (left == n.left && right > n.right));
    }
};

int n = 1001;

vector<Node> sequence(n); //记录所有区间的左右端点
vector<int> record(n); //记录各个集合的最右端点

int main()
{
    std::ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    cin >> n;
    for (int i = 0; i < n; ++i) cin >> sequence[i].left >> sequence[i].right;
    sort(sequence.begin(), sequence.begin() + n);

    int cnt = 0;
    record[0] = sequence[0].right;

    for (int i = 1; i < n; ++i) {
        bool havePos = false; //判断已知的集合是否可以加入当前区间
        //遍历集合的右端点
        for (int j = 0; j <= cnt; ++j) {
            if (sequence[i].left >= record[j]) {
                record[j] = sequence[i].right;
                havePos = true;
                break;
            }
        }
        //遍历完发现没有集合可以加入,那么需要增加一个集合
        if (!havePos) {
            ++cnt;
            record[cnt] = sequence[i].right;
        }
    }

    cout << (cnt + 1) << endl; 

    return 0;
}

但是注意,这种方法在处理POJ 3190的时候会超时,因为考虑特殊情况,如果每个区间一个集合,那么这就是O(n^2)的算法,所以应该考虑用优先级队列来进行优化。

上面的算法之所以会超时,是因为对于整个过程分析的不够细致,因为考虑如果每一次加入新的一头奶牛就要全部对比,这样很耗时间,是否可以利用数据结构来优化这个过程?

首先肯定是要对输入的序列进行排序,那么目前见过的排序技巧有区间调度里面的右端点排序,初始排序左端点升序,左端点相同,右端点升序。

利用优先级队列的优化,每次取出最早结束的时间,如果最早结束的时间小于序列的开始时间,那么就可以加入围栏,否则就需要新开一个围栏。这样就只需要去进行一次比较,我们需要对这个过程进行证明。

首先考虑队列为空和不为空的两种情形。队列为空的情形比较好处理,不为空的情形需要细节分析。

首先队列的优先级是根据结束时间来确定的,结束的时间越早,优先级就越高。那么需要证明为什么只需要去比较一次。考虑最早结束的时间,如果其小于序列的开始时间,那么第二早结束的时间虽然也有可能小于序列开始的时间,所以会不会存在一种情况,就是最早结束的时间和序列开始的时间内可以插入一个新的序列点。答案是不会的,因为排序是按照序列开始时间升序排列的,当前序列的后面的点的开始时间是不会比当前的开始时间早的。那么此时去更新这个结束时间即可(删掉队列原有的点,加入一个新的点)。

如果最早结束的时间大于等于序列的开始时间,因为队列是按照最早结束的时间来确定优先级的,那么意味着队列里的其他的结束时间肯定也比序列的开始时间要大,意味着已经没有围栏合适了,那么就需要去增加一个围栏。

题目特殊的地方是需要“输出路径”,也就是需要按照输入的顺序去输出每头牛被分配的围栏号,所以需要去记录初始的输入顺序和用一个数组record 去记录每头牛被分配的围栏号。