2022-04-14每日刷题打卡

这篇博客探讨了如何使用线段树和优先队列来高效处理动态集合操作。文章通过两个实例展示了这两种数据结构在区间修改和查找最小值方面的应用,包括上帝的集合问题和数组恢复问题。线段树允许在logn时间内完成区间修改和查找,而优先队列则在插入和删除最小值时表现出高效性。此外,还介绍了如何通过预处理避免lz标记的复杂性,简化代码实现。
摘要由CSDN通过智能技术生成

2022-04-14每日刷题打卡

代码源——每日一题

上帝的集合 - 题目 - Daimayuan Online Judge
题目描述

现在上帝有一个空集合,现在他命令你为他执行下列三种操作 n 次,他每次会给你一个操作类型 op。

操作1:向集合中插入一个整数 x;

操作2:将集合中所有的数加上 x;

操作3:输出集合中最小的数,并从集合中将他删除,如果存在多个最小的整数,任意选择一个即可;

输入描述

第一行输入一个整数 n;

接下来的 n 行,每行的输入如下所示。第一个数代表 op,如果 op=1 或 op=2,第二个数代表 xi:

1 xi

2 xi

3

输出描述

如果 op=3,请输出集合中的最小值。

样例输入
7
1 2
1 1
3
1 3
2 5
3
3
样例输出
1
7
8
数据范围

2≤n≤10^6, 1≤xi≤10^12

第一个操作:向集合中插入值。这个操作并不难,有非常多的方法都能做到。

第三个操作:输出最小值并删除。我们利用优先队列或mulset等容器都可以很方便的做到。

问题就是第二个操作,怎么能在短时间内把所有的值都加上1?说到区间修改有的人可能会想到线段树,确实,线段树是个可行的方法,加上lz标记后,我们可以在logn的时间内做到区间修改,并且只要每个点存储的都是区间内的最小值,我们就可以在logn的时间内完成区间修改,logn的时间内找到最小值并将其删除,logn的时间内插入一个元素。

但是lz标记这种东西写起来我觉得太麻烦了!我们有没有办法能做到更简便的完成操作二?

当然是有的,我才不想跑去写lz标记。我们可以假装把所有的元素都加上x,我们用一个变量add存下所有加在全体的x,只要第三次操作输出值的时候把add加上那个值即可。那么有个问题,比如我前面先插入几个元素后全体加10,然后插入了一个很小的元素,这样操作三会输出这个很小的元素,输出的时候会加上add,但是这个元素在逻辑上并没有加上10,怎么办?好解决,我们只要插入的时候,把元素预先减去add再插入即可,比如我这里插入的11,但实际插入的是1。这样在再输出的也是11。至于操作一和三,我们只要找一个能很快找到最小值的容器就行。

1:优先队列(小根堆)

#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<math.h>
#include<set>
#include<numeric>
#include<string>
#include<string.h>
#include<iterator>
#include<map>
#include<unordered_map>
#include<stack>
#include<list>
#include<queue>
#include<iomanip>

#define endl '\n';
typedef long long ll;
typedef pair<ll, ll>PII;
const int N = 1e6 + 10;


int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    ll t,add=0;
    priority_queue<ll,vector<ll>,greater<ll>>que;
    cin >> t;
    while (t--)
    {
        int st;
        cin >> st;
        if (st == 1)
        {
            ll x;
            cin >> x;
            que.push(x-add);
        }
        else if (st == 2)
        {
            ll x;
            cin >> x;
            add += x;
        }
        else
        {
            cout << que.top()+add << endl;
            que.pop();
        }
    }
    
    return 0;
}

2.线段树

这个有点特殊,因为线段树是很难直接加入一个新的点的,但我们可以提前开好空间,这里一共操作数是10^6,假设这全是插入操作,加上保险起见,我们最多开他一个4*1e6+40的线段树,一共有1e6+10个叶子,为了不影响结果,我们把所有的叶子都整成很大的值。然后照着这些位置一个个插入值即可,至于删除操作,我们直接把那个叶子的值改成很多的值即可。

#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<math.h>
#include<set>
#include<numeric>
#include<string>
#include<string.h>
#include<iterator>
#include<map>
#include<unordered_map>
#include<stack>
#include<list>
#include<queue>
#include<iomanip>

#define endl '\n';
typedef long long ll;
typedef pair<ll, ll>PII;
const int N = 1e6 + 10;
ll f[4 * N],len=0;
  
void inserttree(ll k, ll l, ll r, ll x,ll y)
{
    if (l == r)
    {
        f[k] = y;
        return;
    }
    ll m = (l + r) / 2;
    if (x <= m)inserttree(k + k, l, m, x, y);
    else inserttree(k + k + 1, m + 1, r, x, y);
    f[k] = min(f[k + k], f[k + k + 1]);
}

ll calc(ll k, ll l, ll r)
{
    if (l == r)
    {
        ll ans = f[k];
        f[k] = 1e15;
        return ans;
    }
    ll m = (l + r) / 2,res;
    if (f[k + k]<= f[k + k + 1])res = calc(k + k, l, m);
    else res = calc(k + k + 1, m + 1, r);
    
    f[k] = min(f[k + k], f[k + k + 1]);
    return res;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    ll t,add=0;
    cin >> t;
    memset(f, 0x3f3f, sizeof f);
    int a = 0;
    while (t--)
    {
        ll st, x;
        cin >> st;
        if (st == 1)
        {
            cin >> x;
            len++;
            inserttree(1, 1, 1000000, len, x - add);
        }
        else if (st == 2)
        {
            cin >> x;
            add += x;
        }
        else
        {
            cout << 1LL*(calc(1, 1, 1000000)+add) << endl;
        }
    }
    
    return 0;
}

CodeForces——线段树专题

B - Segment Tree, part 1 - Codeforces

This problem is the reversed version of the previous one. There was a permutation pi of n elements, for each ii we wrote down the number ai, the number of j such that j<i and pi<pj. Restore the original permutation for the given ai.

Input

The first line contains the number n (1≤n≤10^5), the second line contains n numbers ai. It is guaranteed that ai were obtained from some permutation using the procedure described in the statement.

Output

Print n numbers, the original permutation.

Example

input

5
0 1 1 0 3

output

4 1 3 5 2 

这题是说,有这么一个数组a,他有个对应的数组b,b的每一位是a的对应位置上,它的左边有多少个大于他的个数,现在他给你了这个数组b,让你求出数组a。

用线段树来写就是,我们从右往左遍历数组b,准备一个线段树,初始叶子都是1。我们每次在线段树里找从右往左数,找第b[i]个1的位置。之后把那个位置的叶子变成0。

为什么这样可以得到结果?首先,我们是从右往左数第b[i]个1的个数,右边的点都是大于左边的点的。而且因为之前数过的叶子都会变成0,说明变成0的叶子在结果的a数组里应该出现在我们当前找的位置的右边,而我们找的是左边有多少个大于我们的个数。所以就要从右往左找第b[i]个叶子的位置。

#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<math.h>
#include<set>
#include<numeric>
#include<string>
#include<string.h>
#include<iterator>
#include<map>
#include<unordered_map>
#include<stack>
#include<list>
#include<queue>
#include<iomanip>

#define endl '\n';
typedef long long ll;
typedef pair<int, int> PII;
const int N = 100050;
int f[4 * N], a[N], n;

void buildtree(int k, int l, int r)
{
    if (l == r)
    {
        f[k] = 1;
        return;
    }
    int m = (l + r) / 2;
    buildtree(k + k, l, m);
    buildtree(k + k + 1, m + 1, r);
    f[k] = f[k + k] + f[k + k + 1];
}

void revise(int k, int l, int r, int x)
{
    if (l == r)
    {
        f[k] = 0;
        return;
    }
    int m = (l + r) / 2;
    if (x <= m)revise(k + k, l, m, x);
    else
        revise(k + k + 1, m + 1, r, x);
    f[k] = f[k + k] + f[k + k + 1];
}

int quire(int k, int l, int r, int x)
{
    if (l == r)
    {
        return l;
    }
    int m = (l + r) / 2;
    if (f[k + k + 1] >= x)return quire(k + k + 1, m + 1, r, x);
    else return quire(k + k, l, m, x - f[k + k + 1]);
}

int main()
{
    cin >> n;
    for (int i = n; i >= 1; i--)
    {
        cin >> a[i];
        a[i]++;
    }
    buildtree(1, 1, n);
    vector<int>v(n);
    for (int i = 1; i <= n; i++)
    {
        v[i - 1] = quire(1, 1, n, a[i]);
        revise(1, 1, n, v[i - 1]);
    }
    for (int i = n - 1; i >= 0; i--)cout << v[i] << " ";
    return 0;
}
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值