0%

算法基础(3)

第一章 基础算法

双指针

算法6:双指针,通常有两种情况:

  • 两个指针分别指向两个序列,如归并排序

  • 两个指针指向同一个序列,可以是一个在首一个在尾,两个指针共同维护一段区间,如快速排序

代码结构一般是:

1
2
3
4
5
6
7
for(i = 0, j = 0; i < n; i++)
{
while(j < i && check(i, j)) //i和j满足某种性质
j++;

//下面就是每道题目的具体逻辑
}

每个指针在所有循环中总共移动的次数不超过$n$,双指针算法就是将朴素的两层for遍历(时间复杂度为$O(n^2)$)优化到$O(n)$。

解题的思路一般是:从朴素做法入手,从中发现一些问题的性质,如单调性等,将原来的$O(n^2)$时间复杂度优化到$O(n)$。

双指针的几个例子:

  1. 读入一行字符串,其中有若干个单词,每个单词中间有一个空格隔开,要求输出每一个单词。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
using namespace std;

int main()
{
char str[1000];
gets(str);

int n = strlen(str);

for(int i = 0; i < n; i++)
{
int j = i;
while(j < n && str[j] != ' ') j++;

//这道题的具体逻辑
for(int k = i; k < j; k++) cout << str[k];
cout << endl;

i = j;
}
return 0;
}
  1. 最长连续不重复子序列:给定一个长度为n的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。

i表示一段区间的右边界,j表示这段区间的左边界。遍历i,找到这段区间最左能到多远,即j的位置,当i向右移时,j一定是向右移或者是不动,一定不会向左移,即是这个问题的单调性,ij在循环走过的长度都不会超过n,因此时间复杂度是$O(n)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
using namespace std;

const int N = 100010;
int a[N], s[N]; //s[]用来存放区间内每个数字出现的次数
int n;

int main()
{
cin >> n;
for(int i = 0; i < n; i++) cin >> a[i];

int res = 0;
for(int i = 0, j = 0; i < n; i++)
{
s[a[i]] ++;
while(s[a[i]] > 1)
{
s[a[j]] --;
j++;
}
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
  1. 数组元素的目标和:给定两个升序排序的有序数组A和B,以及一个目标值x。数组下标从0开始。
    请你求出满足A[i] + B[j] = x的数对(i, j)。数据保证有唯一解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
using namespace std;

const int N = 100010;
int a[N], b[N];

int main()
{
int n, m, x;
cin >> n >> m >> x;

for(int i = 0; i < n; i++) scanf("%d", &a[i]);
for(int i = 0; i < m; i++) scanf("%d", &b[i]);

for(int i = 0, j = m - 1; i < n; i++)
{
while(a[i] + b[j] > x) j--;
if(j >= 0 && a[i] + b[j] == x)
{
cout << i << ' ' << j << endl;
break;
}
}
return 0;
}

位运算

算法7:位运算

算法题中位运算的常用操作:

  • 求一个整数$n$的二进制表示中第$k$位数字是几,如$n=15=(1111)_2$,个位是第0位开始算。

    1. 先把第$k$位移到最后一位,n >> k
    2. 看个位数字是几,n & 1;两步合并就是:n >> k & 1
  • lowbit(x)操作:返回x的最后一位1,如x = 1010,lowbit(x) = 10x = 101000,lowbit(x)=1000

    • lowbit(x)的实现:x & -x,相当于是x & (~x + 1)。(~表示取反)
    • lowbit(x)的应用:统计x中1的个数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
using namespace std;

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

int main()
{
int n;
cin >> n;

while(n--)
{
int x;
cin >> x;

int res = 0;
while(x) x -= lowbit(x), res++;

cout << res << ' ' ;
}
return 0;
}

离散化

算法8:离散化

这里特指整数的离散化,它所针对的问题是:假设有一个数组,其元素个数很少,但元素的值域很大,如a[5]={1, 3, 100, 2000, 500000},我们又需要以这些元素为下标进行一些其他的操作。再开一个500000长度的数组显然是不明智的,因此就需要离散化,将这些值域很大的数字映射到从0开始的连续的自然数。要进行离散化需要考虑两个问题:

  1. a[]中可能有重复元素 $\to$ 去重;
  2. 如何快速地算出a[i]离散化后的值 $\to$ a[]是有序的,即找到数字的下标即可 $\to$ 二分

代码模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vector<int> alls;  //存储所有待离散化的值
sort(alls.begin(), alls.end()); //将所有值排序
alls.eras(unique(alls.begin(), alls.end()), alls.end()); //去掉重复元素

//二分求出x对应的离散化的值
int find(int x) //找到第一个大于等于x的数的下标
{
int l = 0, r = alls.size() - 1;
while(l < r)
{
int mid = l + r >> 1;
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; //映射到1,2,3,...;若要映射到0, 1, 2, 3, ....就return r;
}

例子:假定有一个无限长的数轴,数轴上每个坐标上的数都是0。现在,我们首先进行 n 次操作,每次操作将某一位置x上的数加c。接下来,进行 m 次询问,每个询问包含两个整数l和r,你需要求出在区间[l, r]之间的所有数的和。

数据范围:$-10^9 \le x \le 10^9$,$1\le n, m \le 10^5$,$-10^9 \le l \le r \le 10^9$,$-10000 \le c \le 10000$。

可见相比于整个数轴的范围,要取的数是比较稀疏的,但是跨度很大,这些数的下标就可以用离散化的思想来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

typedef pair<int, int> PII;

const int N = 300010; //n, l, r分别需要10^5,所以需要用到的下标个数就是300000

int n, m;

vector<int> alls; //alls存放所要用到的数的下标,包括赋予了值的数的下标,以及待计算的区间的左右边界的下标
vector<PII> add, query; //add存放取出的数的下标和赋予的值,query存放待计算的区间的左右边界

int a[N], s[N]; //a[]存放之前赋予过的值,s[]是a[]的前缀和,方便计算区间和

int find(int x)
{
int l = 0, r = alls.size() - 1;
while(l < r)
{
int mid = l + r >> 1;
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; //映射到1, 2, 3, ......方便后续的前缀和操作
}

int main()
{
cin >> n >> m;
for(int i = 0; i < n; i++) //读入n行,每行包含两个整数x和c
{
int x, c;
cin >> x >> c;
add.push_back({x, c});
alls.push_back(x);
}

for(int i = 0; i < m; i++)
{
int l, r;
cin >> l >> r;
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}

//对alls序列排序和去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());

//将add中存放的下标和值的数对,下标按照alls排序后的顺序,值填入到数组a[]中,方便后续的前缀和计算
for(auto item : add)
{
int x = find(item.first);
a[x] += item.second;
}

//预处理前缀和,到这一步就和之前讲过的前缀和一样了
for(int i = 1; i <= alls.size(); i++) s[i] += s[i-1] + a[i];

//处理待计算的区间的和
for(auto item : query)
{
int l = find(item.first), r = find(item.second);
cout << s[r] - s[l-1] << endl;
}
return 0;
}

当然可以自己实现unique函数。

1
2
3
4
5
6
7
8
9
10
vector<int>::iterator unique(vector<int> &a)
{
int j = 0;
for(int i = 0; i < a.size(); i++)
if(!i || a[i] != a[i-1])
a[j++] = a[i];

//a[0]~a[j-1]中就存好了a[]中所有不重复的数字
return a.begin() + j;
}

区间合并

算法9:区间合并

若两个区间有交集,那就可以把它们合并到一个较长的区间,可以扩展到多个区间。

例子:给定$n$个区间$[l_i,r_i]$,要求合并所有有交集的区间。注意如果在端点处相交,也算有交集

输出合并完成后的区间个数。例如:[1,3]和[2,6]可以合并为一个区间[1,6]。

数据范围:$1\le n \le 100000$,$-10^9 \le l_i \le r_i \le 10^9$。

解题思路:

  1. 按区间左端点排序;
  2. 扫描整个区间,扫描的过程中把所有有交集的区间合并。每次维护一个当前的区间$[st, ed]$,设已扫描到第$i$个区间$[st_i, ed_i]$,那第$i$个区间和当前的区间的关系有:
    • 第$i$个区间在当前的区间的内部;$\to$ 当前区间不变
    • 第$i$个区间与当前的区间有交集,但不全在其内部;$\to$ 当前区间更新成$[st, ed_i]$
    • 第$i$个区间与当前的区间没有有交集。$\to$ 当前区间更新成$[st_i, ed_i]$

(与区间有关的题目的思路大多都是贪心)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

typedef pair<int, int> PII; //定义一个pair,存放区间的左右端点

void merge(vector<PII> &segs)
{
vector<PII> res;

sort(segs.begin(), segs.end()); //sort先按segs.first排序,即先按区间左端点排序

int st = -2e9, ed = -2e9; //当前维护的区间的左右端点为st, ed
for(auto seg : segs)
if(ed < seg.first) //第i个区间与当前区间没有交集,则可以把当前区间存入res,然后更新当前区间为第i个区间
{
if(st != -2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else ed = max(ed, seg.second); //第i个区间与当前区间有交集,那就更新当前区间的右端点为两者右端点的最大值

if(st != -2e9) res.push_back({st, ed}); //加了一个st != -2e9的判断是为了确保开始输入的区间数量不为0,即初始的st已经变化过了

segs = res;
}

int main()
{
int n;
cin >> n;

vector<PII> segs;
for(int i = 0; i < n; i++)
{
int l, r;
cin >> l >> r;
segs.push_back({l, r});
}

merge(segs);

cout << segs.size() << endl;

return 0;
}