线段树
1)线段树用于求解区间和以及区间最值问题,其算法复杂度为O(logn)。每一个节点代表一个区间,结点的权值代表最值或者区间和。
线段树的思想:线段树的每个叶子节点都是单个元素信息,而每个父节点是其叶子节点信息的整合。即线段树实际上是将信息进行了二进制化处理。(也可以看成分块思想的树化)
可以联系分块算法的处理方式进行同步考虑,分块本身就是一种优雅的暴力算法。
2)线段树的基本操作:线段树的构建、定点删除、定点查询、区间删除、区间查询。
#include<iostream>
using namespace std;
const int maxn = 1000;
//数据结构——线段树
void build(int l, int r, int k);//给序号为k的结点赋值,同时建立其左右孩子
class Node{
public:
int begin, end;
int value;
Node(){}
Node(int a, int b, int c):begin(a), end(b), value(c) {}
void Show(){
cout << begin << " " << end << endl;
cout << "此节点的权值是:" << value;
}
};
Node tree[maxn*4];//用于存储线段树中的值
int a[maxn];//需要进行线段树化的数组,数组下表从零开始
int main(){
for(int i = 1; i <= 3; i++) a[i-1] = i;
build(0, 2, 0);
cout << tree[2].value;
return 0;
}
void build(int l, int r, int k){
if(l == r){
tree[k].begin = l;
tree[k].end = r;
tree[k].value = a[l];
}
else{
int mid = (l+r)/2;
build(l, mid, 2*k+1);
build(mid+1, r, 2*k+2);
tree[k].value = tree[2*k+1].value + tree[2*k+2].value;
}
}
- 注意:要先开辟空间,在进行操作,所以不能在递归结束条件下进行new开辟空间,会引发段错误
- 其他区间查询,区间修改操作之间看下面的模板题就行(主要就是贯彻一个递归和回溯的利用,不是很难)
3)区间更新操作:注意理解mark(或者叫做tag)的设计思想
例如:现在要求更新[x, y]区间上,为每个数都加上k
- 利用单点更新的方法,逐个更新
这样做当然可以做到更新区间,但是复杂度达到了O(nlogn),如果需要依次更新整个数组,复杂度甚至等于重新构建一颗线段树,所以这是我们不能接受的。
为此,我们可以考虑为每个节点设计一个mark字段,用于保存该节点的所有子节点都需要更新mark。相当于采用了延迟更新的思路,先暂时不更新,等到我们需要的时候,再考虑去更新节点的值。
- 采用延迟更新的思路
void update(int i, int x, int y, int k) {
if(该区间与目标区间无关) return;
if(该区间是目标区间的子区间) { // 我们先只是更新当前节点的值,至于其子区间,做上标记即可
tree[i].val += k*(tree[i].end - tree[i].end + 1);
tree[i].addMark(k);
return;
}
// 剩下的情况,就需要继续往下递归查找
push_down(i); // 下面会具体解释该函数的作用
update(x, y, (i<<1), k);
update(x, y, (i<<1)+1, k);
push_up(i); //递归回溯,别忘了将父节点的值也更新一下
}
- push_down的作用
在先后执行两次不同的区间更新操作时,如果不进行push_down,mark的值或许会在查询操作时对结果造成影响。
[3,4]点被标记为mark为2而[3,3]点被标记为5这样的话,被标记的所有的线段,区间就有了重合。在我们进行查询的时候,比如我们查找[2,4]区间的最小值,这时候我们对第五个节点进行更新,由于第五个节点被标记为2,也就意味着他代表的区间内的所有的元素都增加了2,那么最终的最小值就是在原来的基础上增加了2,这时候我们就忽略掉了他的左子树,被标记为5的这个节点,这时候结果就可能不正确了。所以我们必须保证每时每刻被标记的节点都组成一个或多个无重叠的区间,这时候就需要在添加一个操作,就是在对某个节点的子节点进行标记的时候,把本节点的已经被标记过的部分扩展到子节点中,并把本节点的权值更新为子节点的权值的最小值。然后去除本节点的标记。
所以push_down的作用就是:在查询或者当更新节点时mark值遇到冲突的时候使用,将父节点的mark值传递给子节点。
4)区间查询操作:利用递归和二分的思路实现(似乎涉及到树的情况都会这样做!!!)
ll query(ll i, ll x, ll y) {
ll res = 0;
if(x > tree[i].end || y < tree[i].beg) return 0;
if(x <= tree[i].beg && y >= tree[i].end){
return tree[i].value;
}
push_down(i); // 向下将mark标记进行传递
res += query(i<<1, x, y);
res += query((i<<1)+1, x, y);
return res;
}
更详细的内容可以参考:史上最详细的线段树教程
洛谷P3372线段树模板
- 注意:
记得关注题目中给的输入的个数
以及可能出现的最大数的大小,int型有时候并不能够装下题目中出现的数据类型大小。
//P3372线段树1(线段树思想)
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
const int maxn = 100005;
//数据结构——线段树
void build(ll l, ll r, ll k);//给序号为k的结点赋值,同时建立其左右孩子
inline void update(ll l, ll r, ll num, ll k);//为区间l到r上的结点加上k的值,标号为num的结点
inline void push_up(ll num);
inline void push_down(ll num);
ll query(ll l, ll r, ll num);//区间查询操作
class Node{
public:
ll beg, end;//左右区间
ll value;//结点的值
ll mark = 0;//修改的值标记
Node(){}
void addMark(ll value) {
mark += value;
}
void clearMark() {
mark = 0;
}
~Node(){}
};
Node tree[maxn*4];//用于存储线段树中的值
ll a[maxn];//需要进行线段树化的数组,数组下表从1开始
int main(){
ll n, m, opt, l, r, k;
scanf("%lld%lld", &n, &m);
for(ll i = 1; i <= n; i++) scanf("%lld", &a[i]);
build(1, n, 1);//从根节点开始建树
while(m--)
{
scanf("%lld",&opt);
switch(opt)
{
case 1:{
scanf("%lld%lld%lld",&l,&r,&k);
update(l, r, 1, k);
break;
}
case 2:{
scanf("%lld%lld",&l,&r);
printf("%lld\n",query(1, l, r));
break;
}
}
}
return 0;
}
void build(ll l, ll r, ll k){
if(l == r){
tree[k].beg = l;
tree[k].end = r;
tree[k].value = a[l];
tree[k].mark = 0;
}
else{
ll mid = (l+r)/2;
build(l, mid, 2*k);
build(mid+1, r, 2*k+1);
tree[k].beg = tree[2*k].beg;
tree[k].end = tree[2*k+1].end;
tree[k].mark = 0;
tree[k].value = tree[2*k].value + tree[2*k+1].value;
}
}
inline void push_down(ll i) {
ll k = tree[i].mark;
if(k) {
tree[(i<<1)].addMark(k);
tree[(i<<1)+1].addMark(k);
tree[(i<<1)].value += k*(tree[(i<<1)].end - tree[(i<<1)].beg + 1);
tree[(i<<1)+1].value += k*(tree[(i<<1)+1].end - tree[(i<<1)+1].beg + 1);
tree[i].clearMark();
}
}
inline void push_up(ll i){
tree[i].value = tree[(i<<1)].value + tree[(i<<1)+1].value;
}
inline void update(ll x, ll y, ll i, ll k) {
if(tree[i].end < x || tree[i].beg > y) return ;
else if(tree[i].beg >= x && tree[i].end <= y) {
tree[i].value += k*(tree[i].end - tree[i].beg + 1);
tree[i].mark += k; // 这里采用的就是延迟更新的思想
return ;
} else {
push_down(i);
update(x, y, (i<<1), k);
update(x, y, (i<<1)+1, k);
push_up(i);
}
}
ll query(ll i, ll x, ll y) {
ll res = 0;
if(x > tree[i].end || y < tree[i].beg) return 0;
if(x <= tree[i].beg && y >= tree[i].end){
return tree[i].value;
}
push_down(i); // 向下将mark标记进行传递
res += query(i<<1, x, y);
res += query((i<<1)+1, x, y);
return res;
}
洛谷P3374树状数组模板1
模板题主要用于加深对树状数组的理解。
本题主要涉及到区间查询和单点修改,使用树状数组的时间复杂度为O(logn)
- 树状数组tree[],与原数组a[] 的关系:
tree[i] 并不是与原数组中值一一对应的(见下图),只有与另外两个常用的操作同时使用时才有用途。
- 查询Query( i )
返回一个值,这个值等于a[1] + a[2] + … + a[i]; 也就是前缀和
- 更新Update(i, k, n)
作用为修改数组中的值。对于原数组而言,相当于a[i] += k,但是对于树状数组而言,需要根据i的值来具体判断需要更新哪些值,具体操作见下面的代码:
void Update(int i, int k, int n) {
for(; i <= n; i += i&-i){
tree[i] += k;
}
}
ll Query(int i) {
ll sum = 0;
for(; i; i -= i&-i) {
sum += tree[i];
}
return sum;
}
洛谷P1908逆序对——树状数组
利用树状数组的思想求逆序对
主要过程:1. 数据离散化 >> 2. 将数据存入树状数组同时计算逆序对的数量
- 数据离散化
由于需要求逆序对,我们只需要知道数据之间的相对大小关系即可。所以我们可以将所有的数据用连续的整数表示,这样可以节省树状数组的空间。
//例如:
[1, 50, 100] ->离散化后-> [1, 2, 3]
离散化的处理过程也很简单,先设计一个结构体,val保存原数组中的值,num记录出现的顺序。然后按照val的大小关系进行排序,排完序的结构体数组a[1]的含义为:在原数组中大小最小的一个在原数组中的位置出现在a[1].num处。 所以后序只需要将新数组中a[i].num位置的值替换为连续增大的整数即可。也就是ranks数组。
const int maxm = 500005;
int ranks[maxm];
struct point {
int val; int num;
} a[maxm];
//排序数组
inline bool cmp(point x, point y) {
if(x.val == y.val)
return x.num < y.num;
return x.val < y.val;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i].val), a[i].num=i;
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++)
ranks[a[i].num]=i;
//后续只需要将ranks数组里面的值保存到树状数组中即可
}
- 将数据保存到树状数组中同时计算逆序对的数量
首先我们需要明确树状数组的作用:它可以让我们很方便的计算区间和以及单点更新。
在我们已经得到ranks数组之后(ranks数组就是原数组离散后的数组,其中的值为连续的整数,与原数组中元素的大小关系相对应),可以依次将ranks数组中的树加入到树状数组中。
我们每加入一个ranks[i],即与原数组中第i个数大小关系相对应的数。我们可以把这个数看作一个逆序对中的第二个数,所以我们只需要判断比该数先出现的数中有几个比该数大,也就有几个逆序对。
代码如下:
inline void Update(int i, int v) { // 将第i个位置的值加上v
for(; i <= n; i += i&-i) tree[i] += v;
}
inline int Query(int i) { // 查询第i个位置的前缀和
int num = 0;
for(; i; i -= i&-i) num += tree[i];
return num;
}
int main() {
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i].val), a[i].num=i;
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++)
ranks[a[i].num]=i;
//后续只需要将ranks数组里面的值保存到树状数组中即可
for(int i = 1; i <= n; i++){
Update(ranks[i], 1);
ans += i - Query(ranks[i]);
}
cout << ans;
return 0;
}
洛谷P3374树状数组模板2
最常见的树状数组一般常用于求前缀和和单点更新。但是本题需要的操作是区间更新和单点查询。考虑到树状数组便于求前缀和,所以我们可以将树状数组和差分思想联系起来,最终可以将问题得到转化。
对于一个原数组a[]的差分数组b[]而言,a[i] 的值就是b[i]的前缀和。a[x] ~ a[y] 之间每个数增加一个值k,就等价于b[x] + k 和 b[y+1] - k。相当于将区间更新变为双点更新,所以可以考虑用树状数组维护差分数组,这样问题就可以得到很好的解释。
AC代码:
#include<iostream>
using namespace std;
//树状数组加差分
int n, m, now, tmp, opt, x, y , k;
int tree[500005];
inline void Update(int i, int k) {
for(; i <= n; i += i&-i) tree[i] += k;
}
int query(int i) {
int num = 0;
for(; i ; i -= i&-i) num += tree[i];
return num;
}
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> now;
Update(i, now-tmp);
tmp = now;
}
for(int i = 1; i <= m; i++) {
cin >> opt;
if(opt == 1) {
cin >> x >> y >> k;
Update(x, k);
if(y < n) Update(y+1, -k);
} else {
cin >> x;
cout << query(x) << endl;
}
}
return 0;
}
哈希表
- 定义
哈希函数也叫散列函数,它接受一个关键字,然后返回一个哈希地址,用于指示关键字在内存空间中的位置。理想的哈希函数对于不同的输入应该产生不同的结构,同时散列结果应当具有同一性(输出值尽量均匀)和雪崩效应(微小的输入值变化使得输出值发生巨大的变化)。
在理想情况下,哈希表可以做到O(1)的事件复杂度。
- 与一般数组的区别
与普通的列表不同的地方在于,普通列表仅能通过下标来获取目标位置的值,而哈希表可以根据给定的key计算得到目标位置的值。
换句话说,在哈希表这个数据结构中,被存入的数据本身的值和其被存放的位置之间具有对应的函数关系。而在数据中,数据存放的位置和其本身并没有任何关系。
- 冲突的产生
由于哈希函数是关键字到哈希地址的映射关系,并且关键字的数量一般多于哈希地址的数量,(就比如字典中词条的数量多于字典面数一样)所以有时候会导致不同的关键字,通过哈希函数返回的哈希地址却相同的情况。此时,就叫做发生了冲突。对于产生同一哈希地址的不同的关键字,被称作同义字。
- 常用解决冲突的方法
- 开放定址法
- 直接链表法
- 再散列法
leetcode706:设计哈希映射
//设计哈希映射
class MyHashMap {
//采用直接链表法解决冲突
private:
vector<list<pair<int, int> > > data;
static const int base = 729;
int hash(int key){
return key % base;
}
public:
MyHashMap():data(base) {}
void put(int key, int value) {
int Key = hash(key);
for(auto it = data[Key].begin(); it != data[Key].end(); it++){
if((*it).first == key){
(*it).second = value;
return ;
}
}
data[Key].push_back(make_pair(key, value));
}
int get(int key) {
int Key = hash(key);
for(auto it = data[Key].begin(); it != data[Key].end(); it++){
if((*it).first == key){
return (*it).second;
}
}
return -1;
}
void remove(int key) {
int Key = hash(key);
for(auto it = data[Key].begin(); it != data[Key].end(); it++){
if ((*it).first == key) {
data[Key].erase(it);
return;
}
}
}
};
/**
* Your MyHashMap object will be instantiated and called as such:
* MyHashMap* obj = new MyHashMap();
* obj->put(key,value);
* int param_2 = obj->get(key);
* obj->remove(key);
*/
leetcode560:和为k的子数组
- 题目:
给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。
建立一个数组pre[i];用于记录0~i的数的和(前缀和)。然后题目转换成为pre[j] - pre[i] = k的情况。此时可以遍历每个pre[j],我们需要知道在j前面有多少次出现了pre[i],即pre[j] - k。
为了减少时间复杂度,可以建立一个哈希映射。键为pre[i],值为键出现的次数。这样就可以把查询的时间复杂度降低到O(1)。
AC代码如下:(重点理解上面的黑字,也就是什么时候需要用到哈希表)
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int pre = 0;
int count = 0;
unordered_map<int, int> mp;
mp[0] = 1;
for(auto& x : nums){
pre += x;
if(mp.find(pre-k) != mp.end()){
count += mp[pre-k];
}
mp[pre]++;
}
return count;
}
};
leetcode1:两数之和
经典题目就是用哈希表进行处理。
当要查询 target-x 时,可以建立一个哈希表。键为数组中 x 的值,而对应的值为 x 所在数组中下标的位置。这样就可以将每次查询过程中所遇到的 x 值的位置进行保存起来。
使用哈希表之所以可以提升算法的时间复杂度。其本质原因就是因为哈希表可以记录一些杂乱无章的搜索过程中的信息,将这些信息保存起来,提供便捷的查找方式。
洛谷P3370字符串哈希
由于字符串不能够直接作为一个值带入到哈希函数中得到哈希地址,所以我们需要找到一个数字代表一个字符串,并且我们希望每个字符串可以对应不同的数字,也就是说,数字和字符串之间是单射的关系。
联想到进制的知识,可以把字符串的每一位与普通数字的每一位相类比。给出一个进制Base,让字符串的第 i 位乘以进制的 i 次方。
映射关系如下所示:
for(int i = 1; i <= str.length(); i++) hash[i] = (hash[i-1]*Base + str[i])%Mod;