2019.7.15-7.20暑假集训总结

//7.18前三天的等我周日再继续复习总结orz_(:з」∠)_每一天都是充实的欧

7.15:差分与前缀和

 

7.16: 倍增与ST表

 

7.17:并查集进阶

 

7.18:树状数组与线段树

树状数组:

1.引入lowbit(x)    这是膜法。试试你就会发现,这样可以算出 x作为二进制数时,从右往左的第一个1及其以后的数字(没有或者是0吧)。//学长说具体什么原理暂时不需要知道,会用就可以了(嗯嗯这就很工具主义),原理是发明者才需要知道的。。会做题才是真orz 

int lowbit(int i)
{
  return i & -i ;         求数组下标二进制的非0最低位所表示的值
}

比如 11  写成二进制数是  (1011) 

-11=(0100+1)=0101

  0101

&1011

=0001=1=2^0   说明11的非0(就是1)的最低位为个位,右数的第一位

树状数组 c[x],比如c[4],4的二进制是(100),100刚好是3位数,所以这个树状数组到c[4]为止高度是3。

除了树根,c[x]的父节点为 c[x+lowbit(x)]

能用树状数组尽量用树状数组,快,省空间。(一般用来单点更新,区间求和,维护最大值二不能维护最小值。但是求区间最大值的时候复杂度比较高容易TLE,这个时候还是需要用到线段树

 

“细细观察二进制 树状数组追其根本就是二进制的应用”

 

 

kx学长提供的模板&PPT  etc:

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int M =100000+100;
 4 int n,t;
 5 int a[M],C[M];
 6 int lowbit(int x)
 7 {
 8     return x&(-x);
 9 }
10 int sum(int x)
11 {
12     int ret=0;
13     while(x)
14     {
15         ret+=C[x];
16         x-=lowbit(x);
17     }
18     return ret;
19 }
20 void updata(int x,int d)
21 {
22     while(x<=n)
23     {
24         C[x]+=d;
25         x+=lowbit(x);
26     }
27 }
28 int main()
29 {
30     scanf("%d",&t);
31 
32     for(int i=1;i<=t;i++)
33     {
34         printf("Case %d:\n",i);
35         scanf("%d",&n);
36         for(int i=1;i<=n;i++)
37             C[i]=0;
38         for(int i=1;i<=n;i++)
39         {
40             scanf("%d",&a[i]);
41             updata(i,a[i]);
42         }
43         char s[10];
44         while(scanf("%s",s)&&s[0]!='E')
45         {
46             int q,w;
47             scanf("%d%d",&q,&w);
48             if(s[0]=='A')
49                 updata(q,w);
50             else if(s[0]=='S')
51                 updata(q,-w);
52             else
53                 printf("%d\n",sum(w)-sum(q-1));
54         }
55     }
56     return 0;
57 }
树状数组单点更新
 1 #include<iostream>
 2 #include<cstdio>
 3 //#include<bits/stdc++.h>
 4 using namespace std;
 5 typedef long long ll;
 6 const int M = 100000+100;
 7 ll a[M];
 8 ll sum[M];
 9 ll c[2][M];
10 int n;
11 int lowbit(int x)
12 {
13     return x&(-x);
14 }
15 void add(int x,int d,int k)
16 {
17     while(x<=n)
18     {
19         c[k][x]+=d;
20         x+=lowbit(x);
21     }
22 }
23 ll ask(int x,int k)
24 {
25     ll ans=0;
26     while(x)
27     {
28         ans+=c[k][x];
29         x-=lowbit(x);
30     }
31     return ans;
32 }
33 int main()
34 {
35     int q;
36     cin>>n>>q;
37     for(int i =1 ;i <= n;i++)
38     {
39         scanf("%I64d",&a[i]);
40         sum[i]=sum[i-1]+a[i];
41     //    cout<<sum[i]<<endl;
42     }
43     char s[2];
44     int l,r;
45     ll ans=0;
46     for(int i = 1; i <=q; i++)
47     {
48         scanf("%s%d%d",s,&l,&r);
49         if(s[0]=='C')
50         {
51             int d;
52             scanf("%d",&d);
53             add(r+1,-d,0),add(r+1,-(r+1)*d,1);
54             add(l,d,0),add(l,l*d,1);
55         }
56         else
57         {
58             ans=sum[r]+(r+1)*ask(r,0)-ask(r,1);
59             ans-=sum[l-1]+l*ask(l-1,0)-ask(l-1,1);
60             printf("%I64d\n",ans);
61         }
62     }
63     return 0;
64  } 
树状数组区间更新与区间查询

 

 

 

线段树:

满足区加法。

为什么st[M<<2],即数组大小要乘4呢?  当这个线段树是满二叉树的时候,你会发现这棵树它的结点数刚好是   2^x树的高度(最长链的结点数)-1,避免爆栈(RE?),所以直接乘4。

 

 

rt<<1|1  相当于 2*rt+1 ,此处建议自学计算机组成原理。

当build建树的时候,是先向下递归,再从下到上逐步更新。

 1 /*
 2 区间更新   求区间和 
 3 */
 4 //#include<bits/stdc++.h>
 5 #include<cstdio>
 6 #include<iostream>
 7 #include<cstring>
 8 #include<algorithm>
 9 typedef long long ll;
10 using namespace std;
11 #define maxn 100000+100
12 ll sum[maxn<<2];
13 ll lazy[maxn<<2];
14 //int a[maxn<<2];
15 void build(int l,int r,int rt)
16 {
17     lazy[rt]=0;
18     if(l==r)
19     {
20         scanf("%lld",&sum[rt]);
21         return;
22     }
23     int mid=(l+r)/2;
24     build(l,mid,rt<<1);
25     build(mid+1,r,rt<<1|1);
26     sum[rt]=sum[rt<<1]+sum[rt<<1|1];
27 }
28 void pushdown(int l,int r,int rt) 
29 {
30     if(lazy[rt])
31     {
32         lazy[rt<<1]+=lazy[rt];
33         lazy[rt<<1|1]+=lazy[rt];
34         int mid=(l+r)/2;
35         sum[rt<<1]+=(mid-l+1)*lazy[rt];
36         sum[rt<<1|1]+=(r-mid)*lazy[rt];
37         lazy[rt]=0;
38     }
39 }
40 void change(int l,int r,int rt,int L,int R,int c)
41 {
42     if(L<=l&&r<=R)
43     {
44         lazy[rt]+=(ll)c;
45         sum[rt]+=(r-l+1)*(ll)c;
46         return;
47     }
48     pushdown(l,r,rt);
49     int mid=(l+r)/2;
50     if(R>mid)
51     change(mid+1,r,rt<<1|1,L,R,c);
52     if(L<=mid)
53     change(l,mid,rt<<1,L,R,c);
54     sum[rt]=sum[rt<<1]+sum[rt<<1|1];
55 }
56 ll query(int l,int r,int rt,int a,int b)
57 {
58     if(l>=a&&r<=b)
59     return sum[rt];
60     pushdown(l,r,rt);
61     int mid=(l+r)/2;
62     ll ans=0;
63     if(a<=mid)
64         ans+=query(l,mid,rt<<1,a,b);
65     if(b>mid)
66         ans+=query(mid+1,r,rt<<1|1,a,b);
67     return ans;
68 }
69 int main()
70 {
71     int t,n,K;
72     while(~scanf("%d%d",&n,&t))
73     {
74         build(1,n,1);
75         char s[3];
76         while(t--)
77         {
78             int a,b,c;
79             scanf("%s",s);
80             if(s[0]=='C')
81             {
82                 scanf("%d%d%d",&a,&b,&c);
83                 change(1,n,1,a,b,c);
84             }
85             else
86             {
87                 scanf("%d%d",&a,&b);
88                 printf("%lld\n",query(1,n,1,a,b));
89             }
90         }
91     }    
92     return 0;
93 }
线段树区间更新

update的时候,先访问左区间,再访问右区间。 

lazy标记的时候,记得访问后就顺便更新一波,再把最初的懒标记清空为0。

线段树的其他变形应用:逆序对,区间覆盖(把lazy+=d 改成lazy=d),区间最长序列和(区间合并),扫描线,求第k大的某物。

。。

7.19:线段树进阶

1.逆序对   树状数组
遇到了一道题,是poj的Ultra-QuickSort 归并排序。即给出一串数字,求出使用归并排序时数字交换的次数。

归并排序的数字交换次数刚好就是这串数字的逆序数。根据线性代数所学到的知识:一串数字,通过相邻数组两两交换使得数列变得有序的最少交换次数也是逆序数。

例如:5 3 4 2 1

n=5,一共5个数字。

第一步:看5,开一个数组a[n+1](+1方便使用 a[n]这样敏感的位置),  使得a[5]=1;  因为5<=n,它之后数组没有被开辟

第二步:看3,a[3]=1;因为 3<n,所2.以计算一下从 a[n]~a[3+1]的和,看看比它大的数插进来了几个。  ans=a[5]+a[4]=1,那就是5.

第三步:a[4]=1;因为4<n,所以计算一下从a[n]到a[4+1],那就是a[5],所以ans=1。

第四步:a[2]=1;同理 ans=a[5]+a[4]+a[3]=3;

第五步:a[1]=1;同理ans =a[5]+a[4]+a[3]+a[2]=4。

具象化:     5    3    4    2   1

5:                                    1;

3:                          1        ;        

4:                                1   ;

2:                    1               ;

1:               1                    ;

                a[1] a[2]a[3]a[4] a[5]

这里的ans可以开个树状数组记录一下。

代码应用:

 

 

扫描线:

 

 

2.线段树

离散化概念:一开始感觉离散化?nb啊,看不懂什么意思。。。又想起离散数学,心中感慨万千。

离散化是程序设计中一个非常常用的技巧,它可以有效的降低时间复杂度。其基本思想就是在众多可能的情况中“只考虑我需要用的值”。

这一步有点类似于数据分析,就是第一步清洗数据(数据预处理),否则可能会占用太多内存,garbage in ,garbage out。

所以可以先用vector暂时存一下数据,然后用unique去重,在减去首地址就是它的去重后的大小了。但是注意,vector 的目的并非存储本身,而是离散化。

然后,就像把数据按一定大小顺序重新排序这一步也是离散化。比如贪心。。。

 

吉司机线段树/势能线段树:

 

就是这样的题目,不能简单粗暴地用lazy标记,所以要改成 bool flag[i],以确定是否要进行开根号处理,因为这个数比如 153437757 它再大多开几次而且还是向下取整,它也很快就变成1了,所以暴力向下更新即可。据称不超过5次就变成1.

 

权值线段树:

用来找一棵树的前驱,后继,第k大的x,x是第k大。当然,这棵树还可以删除,插入数字。

posted @ 2019-07-19 16:17  鹤花之歌  阅读(189)  评论(1编辑  收藏  举报