set&map

1.1 set 的用法

std::set 是一棵平衡树。学习了平衡树之后,理应清楚 STL 库里这小巧玲珑的平衡树是怎么使用的。

平衡树的内部是一颗红黑树,我们不能直接用 random-access iterator 查找它的有关标号元素,但是我们可以像操作平衡树一样对它进行一些操作。

  • 先介绍一个常用的 STL 方法:把迭代器上的元素取出来,用的是“*”这个符号。这是解除指针的意思,*lower_bound(...) 可以返回一个数字的前驱(这个数字本身)。但是要注意这个东西要保证不会找到 end() 这种地方,否则会出现 UB。
  • 构建平衡树:
    平衡树内的元素必须有定义比较(小于)函数。
  1. 空平衡树
    set<int> s;
    建立一棵空的平衡树。
    时间复杂度 \(O(1)\)
    还可以自己决定比较函数:(其实是第二个参数,如果没有就默认是系统默认的那个)
    set<int, cmp> s;
  2. 对某个序列建立平衡树
    set<int> s(begin, end)
    begin 和 end 可是一个指针或者迭代器。
    时间复杂度:如果数组有序 \(O(len)\),否则 \(O(len \log len)\)
    还可以自己决定比较函数:(其实是第二个参数,如果没有就默认是系统默认的那个)
    set<int, cmp> s(begin, end);
  3. copy 一颗平衡树
    set<int> s(t)
    t 也是一棵平衡树。
    时间复杂度:\(O(n)\)
    operator= 也可以做到 copy 的效果:
    s = t;
  • 插入函数
  1. 普通插入
    s.insert(x);
    时间复杂度:\(O(\log n)\)
  2. 有引导插入
    s.insert(it, x);
    如果 it 就是 x 应该插入的位置,那么为 \(O(1)\)。不过这个东西也没啥用。
  3. 区间插入
    s.insert(begin, end)
    时间复杂度 \(O(n \log (n + size))\)
  4. emplace 函数
    这个比较高级。
    s.emplace(x) 会返回一个 pair。
    如果集合里面已经存在 x,那么会返回 pair<it,false>,其中 it 是这个 x 的位置。
    否则返回 pair<it,true>,其中 it 是新插入这个 x 的位置。
    或许我们用不上这个 it,但是返回插入是否成功总是有用的吧。
    时间复杂度:\(O(\log n)\)(是不是常数更大一些?)
    如果带指引会是线性,但是基本用不上。
    (提一嘴吧,它是 s.emplace_hint(it, x);
  • 删除函数
  1. 普通删除
    s.insert(x);
    时间复杂度:\(O(log n)\)
  2. 有引导删除
    s.insert(it);
    直接瞄准迭代器删。
    时间复杂度:\(O(1)\)
    不过如果你每个操作都能精准找到迭代器也是很厉害了。还要什么 \(O(1)\) 啊。
  3. 区间删除
    s.erase(begin, end);
    删除 \([begin,end)\) 的所有数。
    时间复杂度:\(O(len)\)。(因为这些数都在一起,没有必要进行重新比较)
  • swap
    s.swap(t);
    swap(s,t);
    时间复杂度:\(O(1)\),也就是迭代器换了个指向罢了。
  • clear
    s.clear();
    时间复杂度:\(O(n)\)。因为它是暴力 destroy 整个 set,不会带 \(\log\)

重要的是它作为平衡树的操作!!!!!!!

  • find
    查询一个数的位置。
    s.find(val) 返回一个迭代器,如果不存在这个数就返回 \(s.end()\)(这个性质可以用于查询有没有这个数。)
    实现方法应该是左右儿子那样找吧。
    时间复杂度:\(O(\log n)\)
  • lower_bound;upper_bound
    平衡树自带的查找前驱和后继函数。这东西千万不要和普通的二分搞混了,不然有你 T 的。
    s.lower_bound(x) 返回一个迭代器,为 \(x\) 的前驱。如果不存在这个前驱就返回 \(s.end()\),表示 \(x\) 比 set 里面任何东西都要大!
    时间复杂度:\(O(\log n)\)
    upper_bound 同理!
  • count
    s.count(x) 返回 set 里面 x 的元素个数。这个和 map 同理,都可以用来判断集合里面有没有这个数。
    如果是 multiset 会有好几个元素,返回的可能就不只是 1 或 0 了。
    时间复杂度:\(O(\log n)\)

CF1705E

我们需要维护一个 \(n \log n\) 级别(\(n \le 2 \times 10^5\))的二进制操作,每次对某一位进行 \(+1\)\(-1\)

set 维护区间(也叫珂朵莉树)存储 \((l,r)\) 的集合(\(l\) 为高位),可以进行区间的查找/赋值/合并/拆分操作。
对于本题,我们想到用 set 维护 \(1\) 的区间。怎么进行操作?

如果这一位需要 \(+1\),分两种情况:这位是 \(0\) 的话,插入区间 \((x,x)\) 并且和前后区间合并;这位是 \(1\) 的话,需要进位,找到这个 \(1\) 区间的 \(l,r\),删除它,并插入区间 \((l+1,l+1)\) 并和后面区间合并。\(-1\) 类似讨论即可。其中查找/合并操作的时间复杂度都是 \(O(\log n)\),可以通过本题。

要注意一个细节就是查找的时候要注意有没有可能为 \(end()\),比如这一题中的 \(+1\) 的时候也许加的是最高位,那么就需要判;\(-1\) 不可能减到前面连 \(1\) 都没有的位置,所以不用判。还有一个细节就是我们需要查找一个位置所在区间的更高位,那么我们用高位(\(l\))做第一排序关键字,比较容易判断位置。

CF 上有一个大佬用了很短的代码跑的还很快。他的思路是这样的:单纯加法不能跑满 \(O(n)\),那么我们做加法的时候直接暴力做,并不考虑合并。减法的时候使用合并的写法。这样的常数会小很多,只跑了 300+ ms。 如下(这里 \(r\) 表示高位)

#include<bits/stdc++.h>
using namespace std;
#define l second
#define r first
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
set<pii> s; int a[200010];
void add(int x){
    auto it = s.lower_bound(make_pair(x, 0));
    if(it == s.end() || (*it).l > x) {
        s.insert(make_pair(x,x)); return;
    }
    pii p = (*it);
    if(p.l<=x-1) s.insert(make_pair(x-1,p.l));
    s.erase(it); add(p.r+1);
}
void del(int x){
    auto it = s.lower_bound(make_pair(x, 0));
    pii p = (*it); s.erase(it);
    if(p.l <= x){
        if(p.l<=x-1) s.insert(make_pair(x-1,p.l));
        if(p.r>=x+1) s.insert(make_pair(p.r,x+1));
    }
    else {
        if(p.r>p.l) s.insert(make_pair(p.r,p.l+1));
        s.insert(make_pair(p.l-1,x));
    }
    return;
}
//(r,l)
int main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    int n, q; cin >> n >> q;
    f(i, 1, n) {cin >> a[i]; add(a[i]);}
    f(i, 1, q) {int k,l;cin>>k>>l;del(a[k]);add(l);a[k]=l;cout<<(*(--s.end())).r<<endl;}
    return 0;
}

1.2 map 的用法

std::map 也是一棵平衡树,它存在第一关键字和第二关键字,维护 pii 的时候常数比 std::set 少很多。今天来学学这种平衡树怎么使用。(对比着看,第一行是 set 相关,第二行是 map 相关)

  • 构建平衡树:
    平衡树内的元素必须有定义比较(小于)函数。其中 map 中的元素按照 key 排序。其实很类似 set<pair<...,...>> 并且可以代替其使用。
  1. 空平衡树
    set<int> s;
    map<int, int> s;
    建立一棵空的平衡树。其中 map 必须存在两个元素,一个是 key,一个是 value。
    时间复杂度 \(O(1)\)
    还可以自己决定比较函数:(其实是第二个参数,如果没有就默认是系统默认的那个)
    set<int, cmp> s;
    map<int, int, cmp> s;
  2. 对某个序列建立平衡树
    set<int> s(begin, end);
    map<int, int> s(begin, end);
    begin 和 end 可是一个指针或者迭代器。
    时间复杂度:如果数组有序 \(O(len)\),否则 \(O(len \log len)\)
    还可以自己决定比较函数:(其实是第二个参数,如果没有就默认是系统默认的那个)
    set<int, cmp> s(begin, end);
    map<int, int, cmp> s(begin, end);
  3. copy 一颗平衡树
    set<int> s(t)
    map<int, int> s(t)
    t 也是一棵平衡树。
    时间复杂度:\(O(n)\)
    operator= 也可以做到 copy 的效果:
    s = t;
    不能,也不需要自己决定比较函数。
  • 插入函数
  1. 普通插入
    s.insert(x);
    s.insert(pair<int, int>(1,1));
    时间复杂度:\(O(\log n)\)
  2. 有引导插入
    s.insert(it, x);
    s.insert(it, pair<int, int>(1,1));
    如果 it 就是 x 应该插入的位置,那么为 \(O(1)\)。不过这个东西也没啥用。
  3. 区间插入
    s.insert(begin, end);
    s.insert(begin, end);
    时间复杂度 \(O(n \log (n + size))\)
  4. emplace 函数
    这个比较高级。
    s.emplace(x) 会返回一个 pair。
    如果集合里面已经存在 x,那么会返回 pair<it,false>,其中 it 是这个 x 的位置。
    否则返回 pair<it,true>,其中 it 是新插入这个 x 的位置。
    或许我们用不上这个 it,但是返回插入是否成功总是有用的吧。
    时间复杂度:\(O(\log n)\)(是不是常数更大一些?)
    如果带指引会是线性,但是基本用不上。
    (提一嘴吧,它是 s.emplace_hint(it, x);
    s.emplace(1,1);
    不需要用 pair!!emplace 牛逼!(其实这是该注意的地方)
  • 删除函数
  1. 普通删除
    s.insert(x);
    s.insert(key);
    在 map 中,只需要传入 key 即可。
    时间复杂度:\(O(log n)\)
  2. 有引导删除
    s.insert(it);
    s.insert(it);
    直接瞄准迭代器删。
    时间复杂度:\(O(1)\)
    不过如果你每个操作都能精准找到迭代器也是很厉害了。还要什么 \(O(1)\) 啊。
  3. 区间删除
    s.erase(begin, end);
    s.erase(begin, end);
    删除 \([begin,end)\) 的所有数。
    时间复杂度:\(O(len)\)。(因为这些数都在一起,没有必要进行重新比较)
  • swap
    s.swap(t);
    swap(s,t);
    时间复杂度:\(O(1)\),也就是迭代器换了个指向罢了。
  • clear
    s.clear();
    时间复杂度:\(O(n)\)。因为它是暴力 destroy 整个 set,不会带 \(\log\)
  • []
    map 独有的访问器。(multimap 不存在类似访问器,因为一个 key 可能有多个 value)
    s[i] 返回的是以 i 为 key 的 value 值。

重要的是它作为平衡树的操作!!!!!!!

  • find
    查询一个数的位置。
    s.find(val) 返回一个迭代器,如果不存在这个数就返回 \(s.end()\)(这个性质可以用于查询有没有这个数。)
    实现方法应该是左右儿子那样找吧。
    s.find(key) 返回一个迭代器,相似。并且由于 map 里的元素是以 pair 的方式存储,所以 s.find(key)->first, s.find(key)->second 分别返回 key 和 value 的值。(-> 左边是指针/迭代器)
    时间复杂度:\(O(\log n)\)
  • lower_bound;upper_bound
    平衡树自带的查找前驱和后继函数。这东西千万不要和普通的二分搞混了,不然有你 T 的。
    s.lower_bound(x) 返回一个迭代器,为 \(x\) 的前驱。如果不存在这个前驱就返回 \(s.end()\),表示 \(x\) 比 set 里面任何东西都要大!
    时间复杂度:\(O(\log n)\)
    upper_bound 同理!
    map 里同理。
  • count
    s.count(x) 返回 set 里面 x 的元素个数。这个和 map 同理,都可以用来判断集合里面有没有这个数。
    如果是 multiset 会有好几个元素,返回的可能就不只是 1 或 0 了。
    时间复杂度:\(O(\log n)\)
    map 里同理。

关键的关键在于理解 map 里的元素是按照 key 关键字排的平衡树,并且存储方式是 pair。查找的时候只需要 key,不需要 value。

这个题里面用 set 存储区间,正好把 set<pair<int,int>> 改成 map<int,int>。总时间优化了 2s 左右!

#include<bits/stdc++.h>
using namespace std;
#define l second
#define r first
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
map<int,int> s; int a[200010];
void add(int x){
    auto it = s.lower_bound(x);
    if(it == s.end() || (*it).l > x) {
        s.insert(make_pair(x,x)); return;
    }
    pii p = (*it);
    if(p.l<=x-1) s.insert(make_pair(x-1,p.l));
    s.erase(it); add(p.r+1);
}
void del(int x){
    auto it = s.lower_bound(x);
    pii p = (*it); s.erase(it);
    if(p.l <= x){
        if(p.l<=x-1) s.insert(make_pair(x-1,p.l));
        if(p.r>=x+1) s.insert(make_pair(p.r,x+1));
    }
    else {
        if(p.r>p.l) s.insert(make_pair(p.r,p.l+1));
        s.insert(make_pair(p.l-1,x));
    }
    return;
}
//(r,l)
int main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    int n, q; cin >> n >> q;
    f(i, 1, n) {cin >> a[i]; add(a[i]);}
    f(i, 1, q) {int k,l;cin>>k>>l;del(a[k]);add(l);a[k]=l;cout<<(*(--s.end())).r<<endl;}
    return 0;
}

1.3 进阶用法

  • inserter
    有一些高级的用法会涉及到“插入某一个 set”,这时候直接传迭代器是不行的,你需要传一个叫 inserter 的特殊迭代器。
    std::inserter(x, it) 表示一个指向 it 迭代器的 inserter,it 是在 x 容器内的。

  • copy

copy(bar.begin(), bar.end(), inserter(foo, foo.begin());

  • set_union

set_union(a.begin(), a.end(), b.begin(), b.end(), inserter(foo, foo.begin());
求 a, b 的并集并且插入 foo。

  • set_intersect

set_intersect(a.begin(), a.end(), b.begin(), b.end(), inserter(foo, foo.begin());

求 a, b 的交集并且插入 foo。

CF1764E

【题意】
\(n\) 个颜料,第 \(i\) 个颜料 \(c_i\) 有属性 \((a_i,b_i)\)
你可以按任何顺序在数轴上使用这 \(n\) 个颜料,使用方式是:
在使用第 \(i\) 个颜料的时候,你可以选择:

  • 不大于 \(a_i\) 的一个数并在上面作画,或者
  • 选择一个不大于 \(a_i\) 的已经作画的数 \(j\) 并在数 \(j + b_i\) 上作画。

能否让第 \(1\) 个颜料在数 \(k\) 上作画?

【分析】
首先如果可以在数 \(i\) 上作画,那么也一定能在数 \(\le i\) 上作画。因此只需要考虑最大能在哪个数上作画即可。

考虑倒着贪心,令全集 \(U\) 为可以选择的颜料集合,\(k\) 为目前需要达到的数。

首先最后一步必须使用颜料 \(1\),因此如果 \(a_1 \geq k\) 那么一定能(返回 YES),如果 \(a_1 + b_1 < k\) 那么一定不能(返回 NO)。排除了这两种情况之后,剩下的是 \(a_1 < k\)\(a_1 + b_1 \geq k\),这样需要后面的颜料帮助在 \(\le k - b_1\) 的位置作画。于是 \(k \rightarrow k-b_1\)
此时 \(U\)\(\{c_i|i \in [2,n]\}\)

然后考虑上一步使用的 \(c_i\),它一定要满足 \(a_i + b_i \ge k\)。设满足这个条件且可用(包含在 \(U\) 中)的颜料的集合为 \(T\)
注意到,如果存在 \(a_j \ge a_i\),那么上上步用 \(c_j\) 涂上 \(a_i\),然后上一步用 \(c_i\) 涂上 \(k\),问题就结束了。
考虑若 \(T\) 中包含大于等于两个元素,那么选择 \(a_i\) 最小的那个 \(i\) 即可(返回 YES)。若 \(T\) 中包含零个元素,那么没法操作,一定不可(返回 NO)。故现在唯一要继续讨论下去的情况就是 \(T\) 只包含一个元素的情况。

考虑这个时候只能使用这个元素。记这个元素为 \(c_x\)。若 \(a_x \geq k\),可以直接使用 \(c_x\) 染色,问题解决(返回 YES)。(这个地方容易漏掉,需要发现如果没有这个判断那么最后会出现明明能解决却没有解决的情况)
否则,\(k \rightarrow k - b_x, U \rightarrow U \text{\\} c_x\),递归处理即可。

【实现】

需要维护 \(T\) 集合,那么我们使用一个 set,把全集的排除 \(c_x\) 操作替换成 set 的 erase 操作。并且每次 \(k\) 发生变化的时候,我们要检查哪些元素新加进来了。

考虑检查的过程,是检查 \(a_i + b_i \geq k\) 是否成立。为了时间复杂度正确,考虑先对 \(a_i + b_i\) 从大到小排序,然后利用类似当前弧优化的思想,标记第一个没有满足上一次判断条件的是哪一个 \(c\),下一次从这个开始即可。

另外有一个实现细节:使用 set 维护 \(c\) 时,有可能遇到 \((a,b)\) 一模一样的两个颜料,set 可能会判断他们两个相等(set 判断相等是 \(!(x < y) \&\& !(y < x)\))。这时候我们需要在定义比较函数的时候额外加上一个 index 的比较,代表那两个颜料是不一样的。

注意 set 使用结构体内部比较函数的写法,类型应为 const bool type。

bool operator<(const d &that) const {
    if(a != that.a) return a < that.a;
    else return ind < that.ind;
}

【代码】

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
int n, k; 
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
struct d {
    int a, b;
    int ind;
    bool operator<(const d &that) const {
        if(a != that.a) return a < that.a;
        else return ind < that.ind;
    }
}c[300010];
int cur = 0;
set<d> t;
bool solve(int k) {
    if(t.size() >= 2) return 1;
    else {
        if(t.size() == 0) return 0;
        d x = *(t.begin());
        if(x.a >= k) return 1;
        t.erase(x);
        k -= x.b;
        f(i, cur, n) {
            if(c[i].a + c[i].b >= k) t.insert(c[i]);
            else {
                cur = i;
                break;
            }            
            if(i == n) cur = n + 1;
        }
        return solve(k);
    }
}
bool ab(d x, d y) {return x.a + x.b > y.a + y.b;}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    time_t start = clock();
    //think twice,code once.
    //think once,debug forever.
    int T; cin >> T;
    while(T--) {
        t.clear();
        cur = 2;
        cin >> n >> k;
        f(i, 1, n) cin >> c[i].a >> c[i].b;
        f(i, 1, n) c[i].ind = i;
        sort(c + 2, c + n + 1, ab);
        if(c[1].a >= k) {
            cout << "YES\n";
            continue;
        }
        else if(c[1].a + c[1].b < k) {
            cout << "NO\n";
            continue;
        }
        else {
            k = k - c[1].b;
            f(i, 2, n) {
                if(c[i].a + c[i].b >= k) t.insert(c[i]);
                else {
                    cur = i;
                    break;
                }
                if(i == n) cur = n + 1;
            }
            cout << (solve(k) ? "YES\n": "NO\n");
        }
    }
    time_t finish = clock();
    //cout << "time used: " << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
    return 0;
}
posted @ 2022-07-18 09:36  OIer某罗  阅读(35)  评论(0编辑  收藏  举报