树状数组 (๑•́₃ •)

树状数组 (๑•́₃ •)

一、想说的话

我是学完st表之后学的树状数组,这个概念主要是在st表的基础上增加了动态查询,结合到了前缀和与差分的概念,用到了分块的思想,代码和思想都比较简洁。感觉树状数组最难的点是想到用这个,还是我太不熟练了。写完这些题之后总结一下,目前遇到的和数状数组相关的类型:

  • 动态求区间和
  • 区间加法(也是前缀和那种感觉)
  • 求逆序对
  • 求不同的数的个数
  • 还有上面的情况的扩展,感觉自己距离灵活运用树状数组还有很长的路要去努力

二、【模板】树状数组 1

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  • 将某一个数加上 x
  • 求出某区间每一个数的和

输入格式

第一行包含两个正整数 n,m,分别表示该数列数字的个数和操作的总个数。

第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。

接下来 m 行每行包含 3 个整数,表示一个操作,具体如下:

  • 1 x k 含义:将第 x 个数加上 k
  • 2 x y 含义:输出区间 [x,y][x,y] 内每个数的和

输出格式

输出包含若干行整数,即为所有操作 22 的结果。

输入输出样例

输入 #1

5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4

输出 #1

14
16

说明/提示

【数据范围】

对于 30% 的数据,1≤n≤8,1≤m≤10;
对于 70% 的数据,1≤n,m≤104;
对于 100% 的数据,1≤n,m≤5×105。

样例说明:

img

故输出结果14、16

模板题,代码给出树状数组的模板。这里我写一下我对添加和求和操作的理解。首先说添加,添加他是把所有他的父节点都给加了一遍,然后求和的时候就不太好理解,为啥是每次减去最末尾。我的理解是这样的,其实就是一个倍增的过程。因为它0个个数代表控制的位数,然后消末尾就能保证拿到的刚好是挨着已经算进去的值

看图模拟一下会好理解一些

AC code

#include <bits/stdc++.h>
//#pragma GCC optimize(3,"Ofast","inline")
#define re register
#define ll long long
#define ull unsigned long long
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
int n,m,tree[2000010];
//取最低位
int lowbit(int k){
    return k & -k;
}

void add(int x, int k){
    while(x <= n){
        tree[x] += k;
        x += lowbit(x);
    }
}

int sum(int x){
    int ans = 0;
    while(x != 0){
        ans += tree[x];
        x -= lowbit(x);
    }
    return ans;
}

int main()
{
    ios::sync_with_stdio(false);
    cin >> n>> m;
    for(int i = 1 ; i <= n; i++){
        int a;
        cin >> a;
        add(i, a);
    }
    for(int i = 1; i <= m; i++){
        int a, b, c;
        cin >> a >> b >> c;
        if(a == 1){
            add(b, c);
        }
        if(a == 2){
            cout<<sum(c) - sum(b-1)<<endl;
        }
    }
    return 0;
}

三、【模板】树状数组 2

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数数加上 x
  2. 求出某一个数的值。

输入格式

第一行包含两个整数 NM,分别表示该数列数字的个数和操作的总个数。

第二行包含 N 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。

接下来 M 行每行包含 2 或 4个整数,表示一个操作,具体如下:

操作 1: 格式:1 x y k 含义:将区间 [x,y][x,y] 内每个数加上 k

操作 2: 格式:2 x 含义:输出第 x 个数的值。

输出格式

输出包含若干行整数,即为所有操作 2 的结果。

输入输出样例

输入 #1

5 5
1 5 4 2 3
1 2 4 2
2 3
1 1 5 -1
1 3 5 7
2 4

输出 #1

6
10

说明/提示

样例 1 解释:

img

故输出结果为 6、10。


数据规模与约定

对于 30% 的数据:N≤8,M≤10;

对于 70% 的数据:N≤10000,M≤10000;

对于 100% 的数据:1≤N,M≤500000,1≤x,yn,保证任意时刻序列中任意元素的绝对值都不大于 2^30。

就是在上一道题的基础上加上了差分,现在感觉稍微搞懂一点点树状数组了

#include <bits/stdc++.h>
//#pragma GCC optimize(3,"Ofast","inline")
#define re register
#define ll long long
#define ull unsigned long long
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
int lowbit(int k){
    return k & -k;
}
int tree[500005], n, m;
void add(int x, int k){
    while(x <= n){
        tree[x] += k;
        x += lowbit(x);
    }
}

int sum(int x){
    int ans = 0;
    while(x != 0){
        ans += tree[x];
        x -= lowbit(x);
    }
    return ans;
}

int main()
{
    ios::sync_with_stdio(false);
    n = r;
    m = r;
    int last = 0;
    for(int  i = 1; i <= n; i++){
        int now = r;
        add(i, now - last);
        last = now;
    }
    for(int i = 1; i <= m; i++){
        int op = r;
        if(op == 1){
            int x = r;
            int y = r;
            int k = r;
            add(x, k);
            add(y+1, -k);
        }
        else if(op == 2){
            int x = r;
            cout<<sum(x)<<endl;
        }
    }
    return 0;
}

四、逆序对

题目描述

猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。

最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 ai>aji<j 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。

Update:数据已加强。

输入格式

第一行,一个数 n,表示序列中有 n个数。

第二行 n 个数,表示给定的序列。序列中每个数字不超过 10^9。

输出格式

输出序列中逆序对的数目。

输入输出样例

输入 #1

6
5 4 2 6 3 1

输出 #1

11

说明/提示

对于 25% 的数据,n≤2500

对于 50% 的数据,n≤4×10^4。

对于所有数据,n≤5×105

请使用较快的输入输出

应该不会 O(n^2) 过 50 万吧 by chen_zhe

思路是首先获取每个点的大小排序,然后按大小把他的位置插入到树状数组里面,对于第i个插入的数来说,其他数就两种情况,一种是比他大,不能逆序,那就还没插入(或者相等,但是位置靠后,不会提前插入),或者就是已经插入了(有i-1个),这里又分两种情况,插在前面和插在后面,查在前面的数就是要不然比i小,要不然和i相等但是位置靠前,反正都是不合适的,所以减掉就是第i个数和前面数的总逆序对数

AC code

#include <bits/stdc++.h>
//#pragma GCC optimize(3,"Ofast","inline")
#define re register
#define ll long long
#define ull unsigned long long
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
const int N = 500005;
int tree[N], ran[N], n;

struct point{
    int num, val;
}p[N];

bool cmp(point a, point b){
    if(a.val == b.val){
        return a.num < b.num;
    }
    return a.val < b.val ;
}

int lowbit(int x){
    return x & -x;
}

void add(int x, int k){
    while(x <= n){
        tree[x] += k;
        x += lowbit(x);
    }
}

ll sum(int x){
    ll res = 0;
    while(x != 0){
        res += tree[x];
        x -= lowbit(x);
    }
    return res;
}

int main()
{
    ios::sync_with_stdio(false);
    n = r;
    for(int i = 1; i <= n; i++){
        p[i].val = r;
        p[i].num = i;
    }
    sort(p + 1, p + 1 + n, cmp);
    for(int i = 1; i <= n; i++){
        ran[p[i].num] = i;
    }
    ll ans = 0;
    for(int i = 1; i <= n; i++){
        add(ran[i], 1);
        ans += i - sum(ran[i]);
    }
    cout<<ans<<endl;
    return 0;
}

五、[NOIP2013 提高组] 火柴排队

题目描述

涵涵有两盒火柴,每盒装有 n 根火柴,每根火柴都有一个高度。 现在将每盒中的火柴各自排成一列, 同一列火柴的高度互不相同, 两列火柴之间的距离定义为:∑(aibi)^2

其中 ai 表示第一列火柴中第 i 个火柴的高度,bi 表示第二列火柴中第 i 个火柴的高度。

每列火柴中相邻两根火柴的位置都可以交换,请你通过交换使得两列火柴之间的距离最小。请问得到这个最小的距离,最少需要交换多少次?如果这个数字太大,请输出这个最小交换次数对 10^8−3 取模的结果。

输入格式

共三行,第一行包含一个整数 n,表示每盒中火柴的数目。

第二行有 n 个整数,每两个整数之间用一个空格隔开,表示第一列火柴的高度。

第三行有 n 个整数,每两个整数之间用一个空格隔开,表示第二列火柴的高度。

输出格式

一个整数,表示最少交换次数对 10^8−3 取模的结果。

输入输出样例

输入 #1

42 3 1 43 2 1 4

输出 #1

1

输入 #2

41 3 4 21 7 2 4

输出 #2

2

说明/提示

【输入输出样例说明一】

最小距离是0,最少需要交换 1 次,比如:交换第 1列的前2 根火柴或者交换第 2 列的前 2根火柴。

【输入输出样例说明二】

最小距离是 10,最少需要交换 2 次,比如:交换第 1 列的中间 2 根火柴的位置,再交换第 2 列中后 2 根火柴的位置。

【数据范围】

对于 10% 的数据, 1≤n≤10;

对于 30% 的数据,1≤n≤100;

对于 60% 的数据,1≤n≤10^3;

对于 100% 的数据,1≤n≤10^5,0 ≤ 火柴高度 < 2^31。

对于上道题来说,我感觉难点是怎么从逆序对看出是树状数组,这道题就变成了怎么看出是逆序对。但确实是这样的,只不过定义不太一样了,因为第二个序列又不是按顺序的。第一个序列要和第二个序列大小排名对应才叫顺序,否则为逆序,所以其实就是小改。然后这个题又引起了我对逆序对的思考,我发现上一道题想的可能有点偏,我试着画了一下样例2,我发现逆序这个点其实是应该这么来模拟:因为我上面是按顺序来插入的,如果存在逆序的话,第i个就不会插入到第i个位置,然后第一次是插到后面的,所以没影响,但是等到往前面插的时候,就可以求一个差值,这个差值就是逆序对数量。上道题感觉解释的更好,但只能应用于顺序的情况,像这道题复杂一些的,就只能按我这个思路来了。

AC code

#include <bits/stdc++.h>
//#pragma GCC optimize(3,"Ofast","inline")
#define re register
#define ll long long
#define ull unsigned long long
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
const int N = 500005;
int tree[N], ran[N], n;

struct point{
    int num, val;
}p[N], q[N];

bool cmp(point a, point b){
    if(a.val == b.val){
        return a.num < b.num;
    }
    return a.val < b.val ;
}

int lowbit(int x){
    return x & -x;
}

void add(int x, int k){
    while(x <= n){
        tree[x] += k;
        x += lowbit(x);
    }
}

int sum(int x){
    int res = 0;
    while(x != 0){
        res += tree[x];
        x -= lowbit(x);
    }
    return res;
}
const int M = (1e8 - 3);
int main()
{
    ios::sync_with_stdio(false);
    n = r;
    for(int i = 1; i <= n; i++){
        p[i].val = r;
        p[i].num = i;
    }
    for(int i = 1; i <= n; i++){
        q[i].val = r;
        q[i].num = i;
    }
    sort(p + 1, p + 1 + n, cmp);
    sort(q + 1, q + 1 + n, cmp);
    for(int i = 1; i <= n; i++){
        ran[p[i].num] = q[i].num;
    }
    int ans = 0;
    for(int i = 1; i <= n; i++){
        add(ran[i], 1);
        ans += i - sum(ran[i]);
        ans %= M;
    }
    cout<<ans<<endl;
    return 0;
}

六、 [USACO17JAN]Promotion Counting P

题目描述

The cows have once again tried to form a startup company, failing to remember from past experience that cows make terrible managers!

The cows, conveniently numbered 1…N (1≤N≤100,000), organize the company as a tree, with cow 1 as the president (the root of the tree). Each cow except the president has a single manager (its “parent” in the tree). Each cow ii has a distinct proficiency rating, p(i), which describes how good she is at her job. If cow ii is an ancestor (e.g., a manager of a manager of a manager) of cow j, then we say j is a subordinate of i.

Unfortunately, the cows find that it is often the case that a manager has less proficiency than several of her subordinates, in which case the manager should consider promoting some of her subordinates. Your task is to help the cows figure out when this is happening. For each cow i in the company, please count the number of subordinates j where p(j)>p(i).

奶牛们又一次试图创建一家创业公司,还是没有从过去的经验中吸取教训–牛是可怕的管理者!

为了方便,把奶牛从 1∼n 编号,把公司组织成一棵树,1 号奶牛作为总裁(这棵树的根节点)。除了总裁以外的每头奶牛都有一个单独的上司(它在树上的 “双亲结点”)。

所有的第 i 头牛都有一个不同的能力指数 pi,描述了她对其工作的擅长程度。如果奶牛 i 是奶牛 j 的祖先节点,那么我们我们把奶牛 j 叫做 i 的下属。

不幸地是,奶牛们发现经常发生一个上司比她的一些下属能力低的情况,在这种情况下,上司应当考虑晋升她的一些下属。你的任务是帮助奶牛弄清楚这是什么时候发生的。简而言之,对于公司的中的每一头奶牛 i,请计算其下属 j 的数量满足 pj>pi

输入格式

The first line of input contains N.

The next N lines of input contain the proficiency ratings p(1)…p(N) for the cows. Each is a distinct integer in the range 1,000,000,000.

The next N−1 lines describe the manager (parent) for cows 2…N. Recall that cow 1 has no manager, being the president.

输入的第一行包括一个整数 n

接下来的 n 行包括奶牛们的能力指数 p1,p2…pn。保证所有数互不相同。

接下来的 n−1 行描述了奶牛 2∼n 的上司的编号。再次提醒,1 号奶牛作为总裁,没有上司。

输出格式

Please print N lines of output. The ith line of output should tell the number of subordinates of cow i with higher proficiency than cow ii.

输出包括 n 行。输出的第 i 行应当给出有多少奶牛 i 的下属比奶牛 ii 能力高。

输入输出样例

输入 #1

58042893848469308876816927787146369169577477941123

输出 #1

20100

说明/提示

感谢@rushcheyo 的翻译

【数据范围】
对于 100% 的数据,1≤n≤105,1≤*pi*​≤109。

离散化之类的都和前面的题一样,主要是多了个深搜,注释也很清楚

AC code

#include <bits/stdc++.h>
//#pragma GCC optimize(3,"Ofast","inline")
#define re register
#define ll long long
#define ull unsigned long long
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
const int N = 1e5 + 10;
int n,ans[N], ran[N];
int tree[N];
vector<int> g[N];
struct point{
    int num, val;
}p[N];

int lowbit(int x){
    return x & -x;
}

void add(int x, int k){
    while(x <= n){
        tree[x] += k;
        x += lowbit(x);
    }
    return;
}

int sum(int x){
    int ans = 0;
    while(x != 0){
        ans += tree[x];
        x -= lowbit(x);
    }
    return ans;
}
void dfs(int x){
    //原来比x强的
    ans[x] = -(sum(n) - sum(ran[x]));
    for(auto i : g[x]){
        dfs(i);
    }
    //原来比x强的+下属里面比x强的
    ans[x] += (sum(n) - sum(ran[x]));
    //加自己
    add(ran[x], 1);
}
bool cmp(point a, point b){
    if(a.val == b.val){
        return a.num < b.num;
    }
    return a.val < b.val ;
}
int main()
{
    ios::sync_with_stdio(false);
    n = r;
    for(int i = 1; i <= n; i++){
        p[i].val = r;
        p[i].num = i;
    }
    sort(p + 1, p + 1 + n, cmp);
    for(int i = 1; i <= n; i++){
        ran[p[i].num] = i;
    }
    for(int i = 2; i <= n; i++){
        int fa = r;
        g[fa].push_back(i);
    }
    dfs(1);
    for(int i = 1; i <= n; i++){
        cout << ans[i] << endl;
    }
    return 0;
}

七、[SDOI2009]HH的项链

题目描述

HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。

有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

输入格式

一行一个正整数 n,表示项链长度。
第二行 n 个正整数 ai​,表示项链中第 i 个贝壳的种类。

第三行一个整数 m,表示 H 询问的个数。
接下来 m 行,每行两个整数 l,r,表示询问的区间。

输出格式

输出 m 行,每行一个整数,依次表示询问对应的答案。

输入输出样例

输入 #1

61 2 3 4 3 531 23 52 6

输出 #1

224

说明/提示

【数据范围】

对于 20% 的数据,1≤n,m≤5000;
对于 40% 的数据,1≤n,m≤10^5;
对于 60% 的数据,1≤n,m≤5×10^5;
对于 100% 的数据,1≤n,m,ai​≤10^6,1≤lrn

本题可能需要较快的读入方式,最大数据点读入数据约 20MB

这和前面的题又是完全不同的方向,所以感觉现在对树状数组的应用范围还是没搞太清楚,但这题如果看了树状数组解题的思路,实现倒是不难。这题就是对查询区间的右边界排序,然后走一遍,边走边插入新数字,有重复就删除之前的数字,然后求前缀和之差就行了。这里感觉对重复数字的判断还有点意思,在区间外减掉即可,相当于直接把这个数抹掉了。

AC code

#include <bits/stdc++.h>
//#pragma GCC optimize(3,"Ofast","inline")
#define re register
#define ll long long
#define ull unsigned long long
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
const int N = 1e6 + 5;
int nums[N], n, m;
int have[N];
struct ask{
    int le, ri, pos;
}a[N];
bool cmp(ask a, ask b){
    return a.ri < b.ri;
}
int tree[N], ans[N];
int lowbit(int x){
    return x & -x;
}
void add(int x, int k)
{
    while(x <= n){
        tree[x] += k;
        x += lowbit(x);
    }
}
int sum(int x){
    int res = 0;
    while(x != 0){
        res += tree[x];
        x -= lowbit(x);
    }
    return res;
}
int main()
{
    ios::sync_with_stdio(false);
    n = r;
    for(int i = 1; i <= n; i++){
        nums[i] = r;
    }
    m = r;
    for(int i = 1; i <= m; i++){
        a[i].le = r;
        a[i].ri = r;
        a[i].pos = i;
    }
    sort(a + 1, a + 1 + m, cmp);
    int nxt = 1;
    for(int i = 1; i <= m; i++){
        for(int j = nxt; j <= a[i].ri; j++){
            if(have[nums[j]]){
                add(have[nums[j]], -1);
            }
            add(j, 1);
            have[nums[j]] = j;
        }
        nxt = a[i].ri + 1;
        ans[a[i].pos] = sum(a[i].ri) - sum(a[i].le - 1);
    }
    for(int i = 1; i <= m; i++){
        cout<<ans[i]<<endl;
    }
    return 0;
}

八、[POI2015]LOG

题目描述

维护一个长度为 n 的序列,一开始都是 0,支持以下两种操作:

  1. U k a 将序列中第 k 个数修改为 a
  2. Z c s 在这个序列上,每次选出 c 个正数,并将它们都减去 1,询问能否进行 s 次操作。

每次询问独立,即每次询问不会对序列进行修改。

输入格式

第一行包含两个正整数 n,m,分别表示序列长度和操作次数。

接下来 m 行为 m 个操作。

输出格式

包含若干行,对于每个 Z 询问,若可行,输出 TAK,否则输出 NIE

输入输出样例

输入 #1

3 8U 1 5U 2 7Z 2 6U 3 1Z 2 6U 2 2Z 2 6Z 2 1

输出 #1

NIETAKNIETAK

说明/提示

对于 100% 的数据,1≤n,m≤106,1≤*k*,*c*≤*n*,0≤*a*≤109,1≤s≤10^9。

这种格式又是之前没见过的类型,只能说在我能力范围内,动态维护数组我只能用树状数组。修改操作很简单,就是删除,然后添加。关键是询问的判断。先将大于s的个数求出来,再比较c−大于s的个数乘上c与剩余数之和,若前者大,输出NIE,否则输出TAK

AC code

#include <bits/stdc++.h>//#pragma GCC optimize(3,"Ofast","inline")#define re register#define ll long long#define ull unsigned long long#define r read()using namespace std;//速读inline int read(){	int x=0,f=1;char ch=getchar();	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}	return x*f;}int n, m;const int N = 1e6 + 6;string op[N];int t1[N], t2[N], arr[N];ll tree1[N], tree2[N];int lowbit(int x){    return x & -x;}inline void add(ll *tr, int x, int k){    while(x <= m + 1){        tr[x] += k;        x += lowbit(x);    }}inline ll sum(ll *tr, int x){    ll ans = 0;    while(x != 0){        ans += tr[x];        x ^= lowbit(x);    }    return ans;}int ts[N];int bsearch(int num){ //离散化查询    int l = 1, ri = m + 1, mid;    while(l < ri){        mid = (l + ri) >> 1;        if(ts[mid] < num) l = mid + 1;        else if(ts[mid] > num) ri = mid - 1;        else return mid;    }    return l;}int main(){    ios::sync_with_stdio(false);    cin >> n >> m;    for(int i = 1; i <= m; i++){        cin >> op[i];        cin >> t1[i] >> t2[i];        ts[i] = t2[i];    }    ts[m + 1] = 0;    sort(ts + 1, ts + m + 2);    for(int i = 1; i <= m + 1; i++){        arr[i] = 1;    }    int tbs = bsearch(0);    ll tmp;    add(tree2, tbs, n);    add(tree2, tbs + 1, -n);    for(int i = 1; i <= m; i++){        tbs = bsearch(t2[i]);        if(op[i] == "U"){            add(tree1, arr[t1[i]], -ts[arr[t1[i]]]);            add(tree1, tbs, ts[tbs]);            add(tree2, arr[t1[i]] + 1, 1);            add(tree2, tbs + 1, -1);            arr[t1[i]] = tbs;        }        else{            // 数量和 - 种类数            tmp = (t1[i] - sum(tree2, tbs)) * t2[i];            sum(tree1, tbs - 1) >= tmp ?  cout << "TAK" : cout << "NIE";            cout<<endl;        }    }    return 0;}

九、[JSOI2009]计数问题

题目描述

一个n*m的方格,初始时每个格子有一个整数权值。接下来每次有2种操作:

  • 改变一个格子的权值;
  • 求一个子矩阵中某种特定权值出现的个数。

输入格式

第一行有两个数N,M。

接下来N行,每行M个数,第i+1行第j个数表示格子(i,j)的初始权值。

接下来输入一个整数Q。

之后Q行,每行描述一个操作。

操作1:“1 x y c”(不含双引号)。表示将格子(x,y)的权值改成c(1<=x<=n,1<=y<=m,1<=c<=100)。

操作2:“2 x1 x2 y1 y2 c”(不含双引号,x1<=x2,y1<=y2)。表示询问所有满足格子颜色为c,且x1<=x<=x2,y1<=y<=y2的格子(x,y)的个数。

输出格式

对于每个操作2,按照在输入中出现的顺序,依次输出一行一个整数表示所求得的个数。

输入输出样例

输入 #1

3 31 2 33 2 12 1 332 1 2 1 2 11 2 3 22 2 3 2 3 2

输出 #1

12

说明/提示

数据规模:30%的数据,满足:n,m<=30,Q<=50000

      100%的数据,满足:n,m<=300,Q<=200000

这题难度并不大,不知道为什么我debug快de吐了 ̄へ ̄

因为之前做过类似的题了,这题就是个二维的统计,开个三维数组,统计各种权值的前缀和,然后求个方块就出来了

AC code

#include <bits/stdc++.h>
//#pragma GCC optimize(3,"Ofast","inline")
#define re register
#define ll long long
#define ull unsigned long long
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
int n, m, q, t, x1, x2, op, x, y, c;
int tree[103][303][303];
int mp[303][303];
int lowbit(int x){
    return (x & -x);
}
void add(int t, int x, int y, int num){
    mp[x][y] = t;
    for(int i = x; i <= n; i += lowbit(i)){
        for(int j = y; j <= m; j += lowbit(j)){
            tree[t][i][j] += num;
        }
    }
}
ll sum(int t, int x, int y){
    ll ans = 0;
    for(int i = x; i; i -= lowbit(i)){
        for(int j = y; j; j -= lowbit(j)){
            ans += tree[t][i][j];
        }
    }
    return ans;
}
int main()
{
    ios::sync_with_stdio(false);
    n = r, m = r;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            t = r;
            add(t, i, j, 1);
        }
    }
    q = r;
    for(int i = 1; i <= q; i++){
        op = r;
        if(op == 1){
            x = r, y = r, c = r;
            add(mp[x][y], x, y, -1);
            add(c, x, y, 1);
        }else{
            int y1, y2;
            x1 = r, x2 = r, y1 = r, y2 = r, c = r;
            cout << sum(c, x1-1, y1-1) + sum(c, x2, y2) - sum(c, x2, y1-1) - sum(c, x1-1, y2) << endl;
        }
    }
    return 0;
}

十、[HEOI2012]采花

题目描述

萧薰儿是古国的公主,平时的一大爱好是采花。

今天天气晴朗,阳光明媚,公主清晨便去了皇宫中新建的花园采花。

花园足够大,容纳了 n 朵花,共有 c 种颜色,用整数 1∼c 表示。且花是排成一排的,以便于公主采花。公主每次采花后会统计采到的花的颜色数,颜色数越多她会越高兴。同时,她有一癖好,她不允许最后自己采到的花中,某一颜色的花只有一朵。为此,公主每采一朵花,要么此前已采到此颜色的花,要么有相当正确的直觉告诉她,她必能再次采到此颜色的花。

由于时间关系,公主只能走过花园连续的一段进行采花,便让女仆福涵洁安排行程。福涵洁综合各种因素拟定了 m 个行程,然后一一向你询问公主能采到的花共有几种不同的颜色。

输入格式

输入的第一行是三个用空格隔开的整数,分别代表花的个数 n,花的颜色数 c,以及行程数 m

输入的第二行是 n 个用空格隔开的整数,第 i 个整数代表第 i 朵花的颜色 xi

第 3 行到第 (m+2) 行,每行两个整数 l,r,第 (i+2) 行的数字代表第 i 次行程为第 l 到第 r 朵花。

输出格式

共输出 m 行,每行一个整数。第 i 行的整数代表第 i 次行程公主能采到的花共有几种不同的颜色。

输入输出样例

输入 #1

5 3 51 2 2 3 11 51 22 22 33 5

输出 #1

20010

说明/提示

输入输出样例 1 解释

共有五朵花,颜色分别为 1, 2, 2, 3, 1。

对于第一次行程,公主采花的区间为 [1, 5][1,5],可以采位置 1, 2, 3, 5 处的花,共有 1 和 2 两种不同的颜色。

对于第二次行程,公主采花的区间为 [1, 2][1,2],但是颜色为 1 和 2 的花都只出现了一次,因此公主无花可采。

对于第三次行程,公主采花的区间为 [2, 2][2,2],但是颜色为 2 的花只出现了一次,公主无花可采。

对于第四次行程,公主采花的区间为 [2, 3][2,3],可以采 2, 3 位置的花,只有 2 这一种颜色。

对于第五次行程,公主采花的区间为 [3,5][3,5],但是颜色为 1,2,3 的花都只出现了一次,因此公主无花可采。

数据范围与约定

本题采用多测试点捆绑测试,共有两个子任务

对于子任务 1,分值为 100 分,保证 1≤n,c,m≤3×10^5。

对于子任务 2,分值为100 分,保证 1≤n,c,m≤2×10^6。

对于全部的测试点,保证 1≤xic,1≤lrn

这个题和第七题有一些相似的地方,那个题是求出现的种类,这个是出现两次以上的种类,所以其实就是,来个数组记录上一次出现的种类的位置,等他再出现的时候就更新位置,然后加进去就完了,这里比较绕的是,如果再出现一次怎么办,就是把第一次出现的那个减掉,然后把第二次出现的加上。为什么是第二次?因为现在右边界我们知道包含第三次,但是左边界却不一定包含第二次,所以要在第二次这个地方+1

AC code

#include <bits/stdc++.h>
//#pragma GCC optimize(3,"Ofast","inline")
#define re register
#define ll long long
#define ull unsigned long long
#define r read()
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
const int N = 2e6 + 5;
int n, m, c;
int lowbit(int x){
    return x & -x;
}
int tree[N], a[N], l1[N], l2[N], ans[N];
void add(int x, int k){
    for(int i = x; i <= n; i += lowbit(i)){
        tree[i] += k;
    }
}
struct que{
    int le, ri, pos;
}q[N];
int sum(int x){
    int ans = 0;
    for(int i = x; i; i -= lowbit(i)){
        ans += tree[i];
    }
    return ans;
}
bool cmp(que a, que b){
    return a.ri < b.ri;
}
int main()
{
    ios::sync_with_stdio(false);
    n = r, c = r, m = r;
    for(int i = 1; i <= n; i++){
        a[i] = r;
    }
    for(int i = 1; i <= m; i++){
        q[i].le = r, q[i].ri = r, q[i].pos = i;
    }
    sort(q + 1, q + 1 + m, cmp);
    int j = 1;
    for(int i = 1; i <= m; i++){
        for(; j <= q[i].ri; j++){
            if(!l1[a[j]]){
                l1[a[j]] = j;
            }
            else{
                if(!l2[a[j]]){
                    l2[a[j]] = j;
                    add(l1[a[j]], 1);
                }else{
                    add(l2[a[j]], 1);
                    add(l1[a[j]], -1);
                    l1[a[j]] = l2[a[j]];
                    l2[a[j]] = j;
                }
            }
        }
        ans[q[i].pos] = sum(q[i].ri) - sum(q[i].le - 1);
    }
    for(int i = 1; i <= m; i++){
        cout << ans[i] <<endl;
    }
    return 0;
}

十一、[NOIP2017 提高组] 列队

题目描述

Sylvia 是一个热爱学习的女孩子。

前段时间,Sylvia 参加了学校的军训。众所周知,军训的时候需要站方阵。

Sylvia 所在的方阵中有 n×m 名学生,方阵的行数为 n,列数为 m

为了便于管理,教官在训练开始时,按照从前到后,从左到右的顺序给方阵中 的学生从 1 到 n×m 编上了号码(参见后面的样例)。即:初始时,第 i 行第 j 列 的学生的编号是 (i−1)×m+j

然而在练习方阵的时候,经常会有学生因为各种各样的事情需要离队。在一天 中,一共发生了 q 件这样的离队事件。每一次离队事件可以用数对 (x,y)(1≤xn,1≤ym) 描述,表示第 x 行第 y 列的学生离队。

在有学生离队后,队伍中出现了一个空位。为了队伍的整齐,教官会依次下达 这样的两条指令:

  1. 向左看齐。这时第一列保持不动,所有学生向左填补空缺。不难发现在这条 指令之后,空位在第 x 行第 m 列。
  2. 向前看齐。这时第一行保持不动,所有学生向前填补空缺。不难发现在这条 指令之后,空位在第 n 行第 m 列。

教官规定不能有两个或更多学生同时离队。即在前一个离队的学生归队之后, 下一个学生才能离队。因此在每一个离队的学生要归队时,队伍中有且仅有第 n 行 第 m 列一个空位,这时这个学生会自然地填补到这个位置。

因为站方阵真的很无聊,所以 Sylvia 想要计算每一次离队事件中,离队的同学 的编号是多少。

注意:每一个同学的编号不会随着离队事件的发生而改变,在发生离队事件后 方阵中同学的编号可能是乱序的。

输入格式

输入共 q+1 行。

第一行包含 3 个用空格分隔的正整数 n,m,q,表示方阵大小是 nm 列,一共发 生了 q 次事件。

接下来 q 行按照事件发生顺序描述了 q 件事件。每一行是两个整数 x,y,用一个空 格分隔,表示这个离队事件中离队的学生当时排在第 x 行第 y 列。

输出格式

按照事件输入的顺序,每一个事件输出一行一个整数,表示这个离队事件中离队学生的编号。

输入输出样例

输入 #1

2 2 3 1 1 2 2 1 2 

输出 #1

1
1
4

说明/提示

【输入输出样例 1 说明】

img

列队的过程如上图所示,每一行描述了一个事件。 在第一个事件中,编号为 1 的同学离队,这时空位在第一行第一列。接着所有同学 向左标齐,这时编号为 2 的同学向左移动一步,空位移动到第一行第二列。然后所有同 学向上标齐,这时编号为 4 的同学向上一步,这时空位移动到第二行第二列。最后编号为 1 的同学返回填补到空位中。

【数据规模与约定】

测试点编号nmq其他约定
1∼6≤10^3≤10^3≤500
7∼10≤5×10^4≤5×10^4≤500
11∼12=1≤10^5≤10^5所有事件 x=1
13∼14=1≤3×10^5≤3×10^5所有事件 x=1
15∼16≤3×10^5≤3×10^5≤3×10^5所有事件 x=1
17∼18≤10^5≤10^5≤10^5
19∼20≤3×10^5≤3×10^5\≤3×10^5

数据保证每一个事件满足 1≤xn,1≤ym

这题应该是这些题里面最难的一个了。感觉还需要一些复盘,暂时先放个code。

AC code

#include <bits/stdc++.h>
//#pragma GCC optimize(3,"Ofast","inline")
#define re register
#define ll long long
#define ull unsigned long long
#define r read()
#define mid ((le + ri) >> 1)
using namespace std;
//速读
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
	while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
const int N = 3e5 + 10;
int n, m, q, len, len2, bit1[2 * N], bit2[2 * N], st, ed;
ll p[2 * N];
vector<int> a;
vector<ll> v[N];
struct sl{
    int x, y, id;
    bool flag;
}g[N];
bool cmp1(sl a, sl b){
    if(a.x != b.x){
        return a.x < b.x;
    }
    return a.id < b.id;
}
int lowbit(int x){
    return x & -x;
}
int sum(int x, int *bit){
    int ans = 0;
    while(x > 0){
        ans += bit[x];
        x -= lowbit(x);
    }
    return ans;
}
void add(int x, int y, int *bit){
    while(x <= 600010){
        bit[x] += y;
        x += lowbit(x);
    }
}

bool cmp2(sl a, sl b){
    return a.id < b.id;
}
int main()
{
    ios::sync_with_stdio(false);
    n = r, m = r, q = r;
    len = n;
    for(int i = 1; i <= q; i++){
        g[i].x = r, g[i].y = r;
        g[i].id = i;
        g[i].flag = false;
    }
    for(int i = 1; i <= n; ++i){
        p[i] = 1ll * i * m;
    }
    for(int i = 1; i <= 600010; i++){
        bit1[i] = bit2[i] = lowbit(i);
    }
    sort(g + 1, g + 1 + q, cmp1);
    st = 1, ed = 1;
    while(st <= q){
        //把一行内的出队直接都拉出来
        while(ed < q && g[st].x == g[ed + 1].x){
            ed++;
        }
        a.clear();
        len2 = m;
        for(int i = st; i <= ed; i++){
            //如果在最后一列,标记并跳过
            if(g[i].y == m){
                g[i].flag = true;
                continue;
            }
            int le = 1, ri = len2;
            //预处理真实删除位置
            while(le < ri){
                if(sum(mid, bit2) >= g[i].y){
                    ri = mid;
                }else{
                    le = mid + 1;
                }
            }
            //出列
            add(le, -1, bit2);
            a.push_back(le);
            g[i].y = le;
            len2++;
        }
        //添加到末尾
        for(int i = 0; i < a.size(); i++){
            add(a[i], 1, bit2);
        }
        //扫描下一段
        st = ed + 1;
    }
    sort(g + 1, g + 1 + q, cmp2);
     for(int i = 1; i <= q; ++i){
       ll ans;
       if(!g[i].flag){
           if(g[i].y < m){
               ans = 1ll * (g[i].x - 1) * m + g[i].y;
               printf("%lld\n",ans);
           }else{
               g[i].y = g[i].y - m;
               ans = v[g[i].x][g[i].y];
               printf("%lld\n",ans);
           }
       }

       int le = 1,ri = len;
       while(le < ri){
           if(sum(mid,bit1) >= g[i].x){
               ri = mid;
           }else{
               le = mid + 1;
           }
       }
       add(le,-1,bit1);
       if(!g[i].flag){
           v[g[i].x].push_back(p[le]);
       }else{
           ans = p[le];
           printf("%lld\n",ans);
       }
        p[++len] = ans;
   }
   return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值