干翻线段树——指令集优化指北

前言

在我们刷题的时候,总会碰到一些关于区间操作/修改的题目。这些题目往往要求我们维护一段区间,支持对一段区间进行查询/修改操作。这些题目有如树状数组1一般的简单题,也有如[无聊的数列][2]一般,线段树、树状数组能够完成,但是码量长,可读性差,思考难度大的较难题。这种题目对时间的要求非常严格,询问/查询次数序列长度均在\(10^5\)级别或以上,能够使\(O(n^2)\)的暴力算法望屋窃叹。那么,有没有什么方法,可以通过数据,代码又易于实现呢?

指令集优化

今天我要讲的指令集优化,就是对这个问题的最佳解答。那么,什么是指令集?指令集优化又能干什么?

什么是指令集?

指令集是存储在CPU内部,对CPU运算进行指导和优化的指令集合。

简单来说,我们写的程序,无论是C, C++, Java, Python,还是其它高级语言,CPU都是看不懂的。这个时候,我们的编译器(解释器)把这些语言翻译为汇编代码,进而翻译01编码,即CPU能看得懂的指令集命令

但是,由于高级语言的特性,在向下翻译的过程中,为了保证其正确性兼容性,编译器会产生大量冗余代码。这些代码的存在使程序运行的速度变得慢了许多。

那我能不能把那些冗余代码删掉啊?

通常情况下,这些冗余代码是不能完全弄掉的。比较特殊的方法是开启编译器优化,也就是我们常说的吸氧(-o2),吸臭氧(-o3)。但是,开启了优化,并不代表着冗余代码就全部没有了。冗余代码依然大量存在,我们需要进一步干掉他们。

指令集优化能做什么?

前面的内容,大概可以看出优化的重点:干掉冗余代码

那么,怎么干掉它们呢?

大概有两种方法:

1、内嵌汇编

淦,为了做一道题,我还要专门去学一门新的语言,我是**吗?!!

所以,作为一个蒟蒻,这个方案,pass。

2、指令集优化

想干掉这些冗余代码的可不仅仅是码农和广大OIer。作为运算处理器的供给者,CPU厂商自然也想干掉它们,从而提升自己家产品的运算性能。

在这方面,\(Intel\)为C++提供了一个解决方案:既然优化干不掉冗余代码,那我自己干!

于是,它自己完成了一个利用我家CPU指令集写成的运算库

这个运算库(在这里只讨论为C++编写的)的优点在于,它直接被包含在了一个库文件里,只需调用相应的头文件,就可以直接使用。同时,由于其与CPU的指令集直接相关,生成的冗余代码极少。在我们做题的角度来看,就是“计算同样的加减法,速度快了好几倍”。

指令集优化的运用

前面讲了那么多废话,总结出来一个字:“指令集优化就是快!”

那么,怎么使用呢?

准备工作

注意!注意!注意!请不要在任何正式比赛中使用指令集优化!!

首先,我们要让编译器知道,我要用指令集。

#pragma GCC target("sse,sse2,sse3,ssse3,sse4.1,sse4.2,avx,avx2,popcnt,tune=native")
//这里的SSE, AVX等等都是指令集的名称

其次,我们得导入两个头文件,这样就可以函数形式调用指令集,而不必内联汇编。

#include <immintrin.h>
#include <emmintrin.h>

注意!!当包含了<bits/stdc++.h>时,请先导入这两个头文件,否则会产生库文件冲突!

然后,我们就可以偷税地使用指令集辣!

变量/函数

在这里,常用的变量大概有__m256, __m256i,__m256d三种,分别用于存储单精度浮点型, 整型, 双精度浮点型。其中256是指一个变量占用了256个bit,可以换成128。

而函数,我们可以在Intel的官方手册中找到你想用的函数。这个手册中给出了函数作用的简述,以及大概工作原理的伪代码,非常方便。不过需要一定的外语阅读水平。

在这里面,函数的命名是有规则的,一般为_mm数据大小_运算类型_epi每个元素的大小()。如_mm256_add_epi32(a, b)就是将__m256i类型的变量ab,以int(占32个Bit)为基准,元素块内对应元素相加,返回表示结果的变量。

在这里,我列一些常用的函数(由ouuan整理,侵删)。

__m256i _mm256_set_epi32 (int e7, int e6, int e5, int e4, int e3, int e2, int e1, int e0):参数是八个数,也就是一个“分块”里的数,注意是逆序的。返回值是一个含这八个数的“分块”。

__m256i _mm256_set_epi64x (__int64 e3, __int64 e2, __int64 e1, __int64 e0):和上面一样,只不过是 64 位整数,也就是 long long。

__m256i _mm256_set1_epi32 (int a):相当于 _mm256_set_epi32(a,a,a,a,a,a,a,a)。

__m256i _mm256_add_epi32 (__m256i a, __m256i b):把两个“分块”的对应位置分别相加,返回结果。

__m256i _mm256_cmpeq_epi32 (__m256i a, __m256i b):判断两个“分块”的对应位置是否相等,若相等则返回的“分块”对应位置是 0xffffffff,否则是 0。

__m256i _mm256_cmpgt_epi32 (__m256i a, __m256i b):和上面一样,只不过比较符是大于而不是相等。

__m256i _mm256_and_si256 (__m256i a, __m256i b):返回两个“分块”的按位与,可以配合上面两条比较指令来使用。

tips:

如果想要访问块内的每一个元素,可以通过指针的形式访问。

__m256i block;
int *a = (int *) &block;
for(int i = 0; i < 8; i++)
    printf("%d", a[i]);

实战

这里,我们以一道例题线段树1来讲解其食用方法。

首先,我们先定义一个__m256i数组block[],用于存储数据。

__m256i block[100000];

将输入的数字按照4个一块的方式,全部压进这个数组里。

a = (long long *) & block;
for(int i = 0; i < n; i++)
    scanf("%lld", a + i);

然后是区间增加。这里我们采用的是分块的思想,即“先更改两边,再将中间整块修改”。

void add(int l, int r, long long x) {
    //本文采用的存储方式为a[0..n-1], 区间操作均为前闭后开[l,r)。
    while((l & 3) && (l < r))// 判断边界
        a[l++] += x;
    if(l == r)
        return;
    while((r & 3)) // 判断边界
        a[--r] += x;
    if(l == r)
        return;
    __m256i ad = _mm256_set1_epi64x(x);
    for(l >>= 2, r >>= 2; l < r; l++) {
        block[l] = _mm256_add_epi64(block[l], ad);
    }
}

区间查询,其操作与修改大同小异。

long long ask(int l, int r) {
    long long ans = 0;
    while((l & 3) && (l < r))
        ans += a[l++];
    if(l == r)
        return ans;
    while((r & 3))
        ans += a[--r];
    if(l == r)
        return ans;
    __m256i ans1 = _mm256_set1_epi64x(0);
    for(l >>= 2, r >>= 2; l < r; l++) {
        ans1 = _mm256_add_epi64(block[l], ans1);
    }
    for(int i = 0; i < 4; i++)
        ans += ans1[i];
    return ans;
}

完整代码:

#define __AVX__ 1
#define __AVX2__ 1
#define __SSE__ 1
#define __SSE2__ 1
#define __SSE2_MATH__ 1
#define __SSE3__ 1
#define __SSE4_1__ 1
#define __SSE4_2__ 1
#define __SSE_MATH__ 1
#define __SSSE3__ 1

#pragma GCC optimize("Ofast,no-stack-protector,unroll-loops,fast-math")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4.1,sse4.2,avx,avx2,popcnt,tune=native")

#include <immintrin.h>
#include <emmintrin.h>
#include <bits/stdc++.h>
using namespace std;
__m256i block[100001], mod;
long long *a;
void add(int l, int r, long long x) {
    while((l & 3) && (l < r))
        a[l++] += x;
    if(l == r)
        return;
    while((r & 3))
        a[--r] += x;
    if(l == r)
        return;
    __m256i ad = _mm256_set1_epi64x(x);
    for(l >>= 2, r >>= 2; l < r; l++) {
        block[l] = _mm256_add_epi64(block[l], ad);
    }
}
long long ask(int l, int r) {
    long long ans = 0;
    while((l & 3) && (l < r))
        ans += a[l++];
    if(l == r)
        return ans;
    while((r & 3))
        ans += a[--r];
    if(l == r)
        return ans;
    __m256i ans1 = _mm256_set1_epi64x(0);
    for(l >>= 2, r >>= 2; l < r; l++) {
        ans1 = _mm256_add_epi64(block[l], ans1);
    }
    for(int i = 0; i < 4; i++)
        ans += ans1[i];
    return ans;
}
int main() {
    int n, m, op, x, y, val;
    scanf("%d%d", &n, &m);
    a = (long long *) & block;
    for(int i = 0; i < n; i++)
        scanf("%lld", a + i);
    while(m--) {
        scanf("%d%d%d", &op, &x, &y);
        x--;
        if(op == 1) {
            scanf("%lld", &val);
            add(x, y, val);
        } else {
            printf("%lld\n", ask(x, y));
        }
    }
}

总结

指令集优化的大概就是这样。理论上的复杂度为\(O(nm)\),实际上因为指令集的优化,效率提升数十倍,完全可以通过一些时限较为宽松的题目。当然,其也有巨大的局限性,如不支持区间mod,不能够区间除法(似乎只有在Intel GCC上才能正常运行,其他的编译器会显示“没有此函数”)等等。但是,它依然是一个极好的区间查修解决方法,同时,为以后想要成为码农的同学来说,也是极为有益的。

毕竟,对于一道题目,用不同的方法去解决它,也是锻炼思维的最佳方法之一。

原文地址:https://www.cnblogs.com/MaxDYF/p/11518355.html

时间: 2024-10-09 12:28:51

干翻线段树——指令集优化指北的相关文章

[xsy1129] flow [树链剖分和线段树一起优化网络流][我也不知道这是什么鬼标签]

题面 内部OJ 思路 考虑一个决策方案${x}$,$x_i$表示第$i$个点选不选,$f^k_i$表示点$i$的第$k$个父亲 那么可以得到总花费的表达式$ans=\sum V_i x_i - \sum max(x_i-min(x_{f^1_i},x_{f^2_i},x_{f^3_i},...x_{f^k_i}),0)\ast P_i$ 优化一下表达方式:把收益和支出分开 $ans=\sum_{V_i>0} V_i - \sum_{V_i>0} V_i (1-x_i) - \sum_{V_i&

线段树lazytag优化模板

线段树嘛...很基本的一种数据结构啦 lazytag优化能不错的提高效率 1 #include<bits/stdc++.h> 2 using namespace std; 3 const int MAX=1000000; 4 struct pr { 5 int sum; 6 int lazy; 7 int left,right; 8 }tr[MAX+10]; 9 int n; 10 inline int ll(int k) {return 2*k;} 11 inline int rr(int

Codeforces Round #343 (Div. 2) D. Babaei and Birthday Cake(线段树+离散化优化DP)

题目链接:点击打开链接 题意:给出n个圆柱体的地面半径和高, 要求只能有一个直接放在桌子上, 其他的要放在他上面, 第i个能放在第j个上面的条件是:当且仅当第i个的体积大于第j个且j < i . 求能叠起来的最大体积. 思路:一看就是一个DP, 而且状态很容易表示, d[i]表示到第i个为止能得到的最大总体积.   转移到 max(d[j]) + a[i], (j < i && a[i] > a[j]).  但是n非常大, 显然要优化, 因为第二层循环所做的事情就是在i之

CDOJ 1335 郭大侠与“有何贵干?” (线段树&amp;扫描线) - xgtao -

郭大侠与“有何贵干?” 题意 题目给出n(<=100000)个长方体,给的是左下角和右上角的坐标x,y(1<=x,y<=1000000000),z(1<=z<=3),求刚好覆盖k次的体积,答案保证在long long 之内 题解 1.根据数据范围首先要想到要离散化. 2.求覆盖的面积或者体积会想到线段树维护扫描线,求体积难道要用二维线段树?这一道题是可以不的,因为1<=z<=3所以可以把体积拆开当做面积来算,当1<=z<=2就是求前面覆盖K次的面积,当

【BZOJ3073】[Pa2011]Journeys 线段树+堆优化Dijkstra

[BZOJ3073][Pa2011]Journeys Description Seter建造了一个很大的星球,他准备建造N个国家和无数双向道路.N个国家很快建造好了,用1..N编号,但是他发现道路实在太多了,他要一条条建简直是不可能的!于是他以如下方式建造道路:(a,b),(c,d)表示,对于任意两个国家x,y,如果a<=x<=b,c<=y<=d,那么在xy之间建造一条道路.Seter保证一条道路不会修建两次,也保证不会有一个国家与自己之间有道路. Seter好不容易建好了所有道路

hdu 5266 pog loves szh III 在线lca+线段树区间优化

题目链接:hdu 5266 pog loves szh III 思路:因为它查询的是区间上的lca,所以我们需要用在线lca来处理,达到单点查询的复杂度为O(1),所以我们在建立线段树区间查询的时候可以达到O(1*nlgn)的时间复杂度 ps:因为栈很容易爆,所以.....你懂的 -->#pragma comment(linker, "/STACK:1024000000,1024000000") /*****************************************

HDU 5828 Rikka with Sequence (线段树+剪枝优化)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5828 给你n个数,三种操作.操作1是将l到r之间的数都加上x:操作2是将l到r之间的数都开方:操作3是求出l到r之间的和. 操作1和3就不说了,关键是开方操作. 一个一个开方,复杂度太高,无疑会T.所以我们来剪枝一下. 我们可以观察,这里一个数最多开方4,5次(loglogx次)就会到1,所以要是一段区间最大值为1的话,就不需要递归开方下去了.这是一个剪枝. 如果一段区间的数都是一样大小(最大值等于

Codeforces 833B 线段树优化 dp

Codeforces  833B  The Bakery 题意: n 个数要分成 k 块,每块的价值是其不同数的个数,问价值和最大是多少. tags: dp[i][j]表示前 j 个数分成 i 块的最大权值和,转移: dp[i][j] = max( dp[i-1][k] + val[k+1][j] ) , k是 1~j . 但这个过程其实并不好转移,要利用累加的特点,用线段树进行优化 (感觉我不看题解是想不到的,2333) 大概就是,对于第 i 层,我们假定已经知道了第 i-1 层,也就是求出了

[CF787D]遗产(Legacy)-线段树-优化Dijkstra(内含数据生成器)

Problem 遗产 题目大意 给出一个带权有向图,有三种操作: 1.u->v添加一条权值为w的边 2.区间[l,r]->v添加权值为w的边 3.v->区间[l,r]添加权值为w的边 求st点到每个点的最短路 Solution 首先我们思考到,若是每次对于l,r区间内的每一个点都执行一次加边操作,不仅耗时还耗空间. 那么我们要想到一个办法去优化它.一看到lr区间,我们就会想到线段树对吧. 没错啦这题就是用线段树去优化它. 首先我们建一棵线段树,然后很容易想到,我们只需要把这一棵线段树当做