第一章:基础算法

排序

快速排序

acwing-785 快速排序

题目描述
给定你一个长度为\(n\)的整数数列。请你使用快速排序对这个数列按照从小到大进行排序。并将排好序的数列按顺序输出。

输入格式
输入共两行,第一行包含整数\(n\)。第二行包含\(n\)个整数(所有整数均在\(1\sim 10^9\)范围内),表示整个数列。

数据范围
\(1 \le n \le 1000000\)

输入样例:

5
3 1 2 4 5

输出样例:

1 2 3 4 5
#include <iostream>
using namespace std;

const int N = 106+10;
/*
注意当模板内为quick_sort(q, l, j), quick_sort(q, j + 1, r);时,x不能为q[r]或q[l + r + 1 >> 1]。
当模板中为quick_sort(q, l, i - 1), quick_sort(q, i, r);时,x不能为q[l]或q[l + r >> 1]。
*/

int n, q[N];

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

int main(){
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &q[i]);
    quick_sort(q, 0, n - 1);
    for (int i = 0; i < n; i++)
        printf("%d ", q[i]);
    printf("\n");
    return 0;
} 

acwing-786 第k个数

题目

给定一个长度为n的整数数列,以及一个整数k,请用快速选择算法求出数列从小到大排序后的第k个数。

输入格式

第一行包含两个整数n和k。第二行包含n个整数(所有整数均在\(1 \sim 10^9\)范围内),表示整数数列。

输出格式

输出—个整数,表示数列的第k小数。

数据范围

\(1 \le n \le 100000, 1 \le k\le n\)

输入样例:

5 3
2 4 1 5 3

输出样例:

3
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;

int n, k;
int q[N];

int find_k(int q[], int l, int r, int k) {

    if (l >= r) return q[r];

    int i = l - 1, j = r + 1;
    int pivot = q[l + r >> 1];

    while (i < j) {
        while (q[++i] < pivot);
        while (q[--j] > pivot);
        if (i < j) swap(q[i], q[j]);
    }
    int sl = j - l + 1;
    if (sl >= k) return find_k(q, l, j, k);
    else return find_k(q, j + 1, r, k - sl);
}

int main()
{
    cin >> n >> k;
    for(int i = 0; i < n; i ++) cin >> q[i];
    cout << find_k(q, 0, n - 1, k) << endl;
    return 0;
}

归并排序

acwing-787 归并排序

题目描述

给定你一个长度为 n 的整数数列。请你使用归并排序对这个数列按照从小到大进行排序。并将排好序的数列按顺序输出。

输入格式
输入共两行,第一行包含整数\(n\)。第二行包含\(n\)个整数(所有整数均在\(1\sim 10^9\)范围内),表示整个数列。

数据范围
\(1 \le n \le 1000000\)

输入样例:

5
3 1 2 4 5

输出样例:

1 2 3 4 5
#include <iostream>
using namespace std;

const int N = 106+10;

int n, q[N],tmp[N];

void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}


int main(){
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &q[i]);
    merge_sort(q, 0, n - 1);
    for (int i = 0; i < n; i++)
        printf("%d ", q[i]);
    printf("\n");
    return 0;
} 

acwing-788 逆序对的数量

题目

给定一个长度为n的整数数列,请你计算数列中的逆序对的数量。逆序对的定义如下:对于数列的第i个和第j个元素,如果满足i<j且a[i]> a[j],则其为一个逆序对;否则不是。

输入格式

第一行包含整数n,表示数列的长度。第二行包含n个整数,表示整个数列。

输出格式

输出一个整数,表示逆序对的个数。

数据范围

\(1\le n\le 100000,数列中元素的取值范围[1\sim10^9]\)

输入样例:

6
2 3 4 5 6 1

输出样例:

5
#include <iostream>
using namespace std;
typedef long long LL;

const int N = 100010;

int n;
int q[N],tmp[N];

/*
因为逆序对最多为(n-1)*n/2个,为O(n^2)数量级。
因为n最大为10^5,所以逆序对最多为10^10个,超出了int的范围,所以要用long long
*/
LL merge_sort(int l, int r)
{
    if(l >= r) return 0;
    int mid = l + r >> 1;
    LL res = merge_sort(l,mid) + merge_sort(mid + 1, r);

    // 1. 归并的过程
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
    // 这个If表示是顺序的
        if (q[i] <= q[j]) tmp[k++] = q[i++];
        // 如果存在逆序对..
        else
        {
            tmp[k ++] = q[j ++];
            // 2. 就跟归并过程是一样的,归并的过程当中统计个数
            res += mid - i + 1;
        }
        while (i <= mid) tmp[k ++] = q[i ++];
        while (j <= r) tmp[k ++] = q[j ++];
            // 3.1把tmp j 写到 q里面去
        for (i = l, j = 0; i <= r; i ++, j ++) q[i] = tmp[j];
        return res;
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++) cin >> q[i];
    cout << merge_sort(0, n - 1) << endl;
    return 0;
} 

二分

整数二分

题目如果有单调性就一定可以二分,但是可以二分的题目不一定非得有单调性。

这类题目一般有一个序列,这个序列一部分满足某种性质,一部分不满足该性质,满足性质和不满足性质的两个子区间的边界。一般是找左右边界。

image-20220809002712443

模板如下:

bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{ 
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

acwing-789 数的范围

题目

给定一个按照升序排列的长度为n的整数数组,以及 q 个查询。对于每个查询,返回一个元素k的起始位置和终止位置(位置从0开始计数)。如果数组中不存在该元素,则返回“-1 -1”。

输入格式
第一行包含整数n和q,表示数组长度和询问个数。第二行包含n个整数(均在1~10000范围内),表示完整数组。接下来q行,每行包含一个整数k,表示一个询问元素。

输出格式
共q行,每行包含两个整数,表示所求元素的起始位置和终止位置。如果数组中不存在该元素,则返回“-1 -1”。

数据范围
\(1\le n\le 100000, 1\le q\le 10000, 1\le k\le 10000\)

输入样例:

6 3
1 2 2 3 3 4
3
4
5

输出样例:

3 4
5 5
-1 -1
#include <iostream>
using namespace std;

const int N = 105+10;

int n, m, q[N];

int main(){
	scanf("%d%d",&n, &m);
	for(int i = 0;i < n;i++) scanf("%d", &q[i]);
	
	while(m--){
		int x;
		scanf("%d", &x);
		
		int l = 0, r = n - 1;
		// 假设check函数为:判断输入y>=x?。即找到>=x的第一个数。
		while(l < r){
			int mid = l + r >> 1;
			if(q[mid] >= x) r = mid; 
			else l = mid + 1;
		} 
		
		if(q[l]!=x) cout<<"-1 -1"<<endl;
		else{
			 cout << l << " ";
			 
			 //假设check函数为:判断输入y<=x?。即找到<=x的最后一个数。
			 int l = 0, r = n - 1;
			 while(l < r){
			 	int mid = l + r + 1 >> 1;
			 	if(q[mid] <= x) l = mid;
			 	else r = mid - 1;
			 }
			 cout<< l <<endl;
		}
	}
	return 0;
}

浮点数二分

浮点数二分不需要考虑边界问题,比整数二分要简单。浮点数二分是通过二分法找到一个区间,当区间小于指定精度时(\(r-l\le10^{-6}\)),就可以认为我们找到了答案。当区间足够小时,就可以把区间看成一个数。

如果题目要求保留四位小数,区间要满足\(r-l \le 10^{-6}\);如果题目要求保留五位小数,区间要满足\(r-l \le 10^{-7}\);如果题目要求保留六位小数,区间要满足\(r-l \le 10^{-8}\),即区间精度要比题目要求精度多二位。

例题:找一个数的立方根,答案要求保留六位小数。

#include <iostream>
using namespace std;

int main(){
	double x;
	cin>>x;
	double l = 0, r = x;
	while(r - l >1e-8){ // 题目要求保留六位小数,因此r-l>10^{-8}时要继续计算。
		double mid = (l + r) / 2;
		if(mid * mid <=x) l = mid;
		else r = mid;
	}
	printf("%lf\n", l);
	return 0;
} 

acwing-790 数的三次方根

题目

给定一个浮点数n,求它的三次方根。

输入格式

共一行,包含一个浮点数n。

输出格式

共一行,包含一个浮点数,表示问题的解。注意,结果保留6位小数。

数据范围

\(-10000\le n\le 10000\)

输入格式

1000.00

输出格式

10.000000
#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    double n;
    cin >> n;
    
    double l = -10000, r = 10000;
    
    while (r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= n) r = mid;
        else l = mid;
    }
    
    printf("%.6lf\n", l);
}

高精度

高精度加法

acwing-791 高精度加法

题目

给定两个正整数(不含前导0),计算它们的和。

输入格式

共两行,每行包含一个整数。
输出格式
共一行,包含所求的和。

数据范围
\(1\le 整数长度\le 100000\)

输入样例:

12
23

输出样例:

35
#include <iostream>
#include <vector>
 
using namespace std;
const int N = 1e6 + 10;//定义一个常量


vector<int> add(vector<int> &A, vector<int> &B) {
	vector<int> c;//定义动态整形数组c
	int t = 0;//t为余项
	for (int i = 0; i < A.size() || i < B.size(); i++) {
		if (i < A.size()) t += A[i];//如果A数组还有数字就加上
		if (i < B.size()) t += B[i];//如果B数组还有数字就加上
		c.push_back(t % 10);//取余数
		t /= 10;//t整除10,作为进项
	}
	if (t) c.push_back(1);//如果最后结束了,t还有,就再进1
	return c;//返回c数组
}
int main() {
	string a, b;//定义a,b为字符串
	vector<int> A, B;//定义动态整形数组A,B
	cin >> a >> b;
	for (int i = a.size() - 1; i >= 0; i--) {
		A.push_back(a[i] - '0');//将a字符串逆序赋值给A数组(注意这里(a[i]-'0')将数字字母转化为数字(原理为用a[i]的ASCII码值减去0的ASCII码值)
	}
	for (int i = b.size() - 1; i >= 0; i--) {
		B.push_back(b[i] - '0');//操作同上
	}
	
	//auto能够自动识别右边的数据类型,使用auto要注意c++版本,c++11后才支持。 
	auto C = add(A, B);//进行处理
	for (int i = C.size() - 1; i >= 0; i--) cout << C[i];//输出
	return 0;
}

高精度减法

acwing-792 高精度减法

题目

给定两个正整数(不含前导0),计算它们的差,计算结果可能为负数。

输入格式

共两行,每行包含一个整数。

输出格式

共—行,包含所求的差。

数据范围

\(1≤整数长度≤10^5\)

输入样例:

32
11

输出样例:

21
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e6 + 10;

// 判断是否 A>= B
bool cmp(vector<int> &A, vector<int> &B)
{
    if (A.size() != B.size()) return A.size() > B.size();
    for (int i = A.size() - 1; i >= 0; i --)
        // 找到第一位不相等的,判断大小。
        if (A[i] != B[i])
            return A[i] > B[i];
        // 否则就相等,谁先谁后都行。
    return true;
}

vector<int> sub(vector<int> &A, vector<int> &B)
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < A.size(); i ++) //输入前保证了A的长度一定大于或等于B的长度,所以只要考虑A的长度即可。 
    {
        // 6.1 仍然是从个位开始减的
        // 6.2 t是借位,t为正。因为算法是大的减去小的,所以永远足够减。
        t = A[i] - t;
        if (i < B.size()) t =t - B[i]; 
        // 6.3 t无论正负都满足
        c.push_back ((t+10)%10); //当t为正数,则(t+10)%10为原值;t为负数,则t+10就相当于借1,然后模10后仍为原值。 
        // 6.4 更新下借位信息
        if (t < 0) t = 1;
        else t = 0;
    }
    // 去掉最高位的0
    while (c.size() > 1 && c.back() == 0) c.pop_back();
    return c;
}

int main ()
{
    // 1. 用字符串读进来
    string a , b;
    // 2. 存储到vector里面去
    vector<int> A, B;
    
    cin >> a >> b;
    
    // 3 逆序存入到数组当中
    // 4. 存数字进去所以 - '0'
    for (int i = a.size() - 1; i >= 0; i --) A.push_back(a[i] - '0');
    for (int i = b.size() - 1; i >= 0; i --) B.push_back(b[i] - '0');
    // 5.总是保证大数-小数
    if (cmp(A, B))
    {
    // 6.
        auto c = sub (A, B);
        for (int i = c.size() - 1; i >= 0; i --) printf("%d",c[i]);
    }
    else 
    {
        auto c = sub (B, A);
        printf("-");
        for (int i = c.size() - 1; i >= 0; i --) printf("%d",c[i]);
    }
    return 0;
}

高精度乘法

主要是大数乘以一个较小的数。

acwing-793 高精度乘法

题目

给定两个非负整数(不含前导0)A和B,请你计算\(A\times B\)的值。

输入格式

共两行,第一行包含整数A,第二行包含整数B。

输出格式

共一行,包含\(A\times B\)的值。数

据范围

\(1\le A的长度\le 100000, 0<B\le 10000\)

输入样例:

2
3

输出样例:

6
#include <iostream>
#include <vector>
using namespace std;
// C = A * b, A >= 0, b >= 0
vector<int> mul(vector<int> &A, int b)
{
    vector<int> C;

    int t = 0;
    for (int i = 0; i < A.size() || t; i ++ )
    {
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }
    
    //去除前置0
    while (C.size() > 1 && C.back() == 0) C.pop_back();

    return C;
}

int main()
{
    string a;
    int b;
    cin >> a >> b;
    vector<int> A;
    for(int i = a.size()-1; i >= 0; i--) A.push_back(a[i] - '0');
    auto C = mul(A, b);
    for(int i = C.size()-1; i >= 0; i--) cout << C[i];
    cout << endl;
    return 0;
}

高精度除法

主要是一个大数除以一个较小的数

acwing-794 高精度除法

题目

给定两个非负整数(不含前导0)A,B,请你计算A/B的商和余数。

输入格式
共两行,第一行包含整数A,第二行包含整数B。

输出格式

共两行,第一行输出所求的商,第二行输出所求余数。

数据范围

\(1\le A的长度\le 100000,1<B<10000, B一定不为0\)

输入样例:

7
2

输出样例:

3
1
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

//   A/b   商是c  余数是r
vector<int> div(vector<int> &A, int b, int &r) { //r是引用
	vector<int> C;  //商
	r = 0;//余数

	//除法是从最高位开始
	for(int i = A.size()-1; i >= 0; i--) {
		r = r * 10 + A[i];  //把各位留出,再加上这一位的各位
		C.push_back(r / b);  //商 是这一位整除b
		r %= b;   //余数
	}

	reverse(C.begin(), C.end()); //因为最开始push的是c的最高位,和我们前面定义的大整数存法是反过来的
	//前面定义的大整数乘法是c[0]在最低位,是反过来的。所以要reverse一遍
	while(C.size() > 1 && C.back() == 0)  C.pop_back();  //商可能存在前导0
	return C;
}

int main() {
	string a;  //a很长,数位多     b很小,数位少
	int b;
	cin >> a >> b;
	vector<int> A;
	for(int i = a.size()-1; i >= 0; i--) A.push_back(a[i] - '0');
	int r;//余数
	auto C = div(A, b, r);
	for(int i = C.size()-1; i >= 0; i--) cout << C[i]; //输出商
	cout << endl;
	cout << r << endl;  //输出余数
	return 0;
}

前缀和

前缀和的下标从1开始,而不是0。

acwing-795 前缀和

题目

输入一个长度为n的整数序列。接下来再输入m个询问,每个询问输入一对l, r。对于每个询问,输出原序列中从第l个数到第r个数的和。

输入格式

第一行包含两个整数n和m。

第二行包含n个整数,表示整数数列。

接下来m行,每行包含两个整数l和r,表示一个询问的区间范围。

输出格式

共m行,每行输出一个询问的结果。

数据范围
\(1\le l\le r\le n, 1\le n,m\le 100000, −1000\le 数列中元素的值\le 1000\)

输入样例:

5 3
2 1 3 6 4
1 2
1 3
2 4

输出样例:

3
6
10
#include<iostream>
#include<cstdio>

using namespace std;
const int N=1e5+10;
int a[N],sum[N];

int main() {
	int n,m,x;
	cin>>n>>m;
	// 通过公式计算前缀和数组。
	for(int i=1; i<=n; i++) {
		cin>>x;
		sum[i]=x+sum[i-1];
	}
	while(m--) {
		int l,r;
		cin>>l>>r;
		// s[r]-s[l-1]=s [l]+s[l+1]+...+s[r] 
		cout<<sum[r]-sum[l-1]<<endl;
	}
	return 0;
}

acwing-796 子矩阵的和

题目

输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数\(x_1, y_1, x_2, y_2\),表示一个子矩阵的左上角坐标和右下角坐标。对于每个询问输出子矩阵中所有数的和。

输入格式

第一行包含三个整数n,m,q。

接下来n行,每行包含m个整数,表示整数矩阵。

接下来q行,每行包含四个整数\(x_1, y_1, x_2, y_2\),表示一组询问。

输出格式
共q行,每行输出一个询问的结果。

数据范围
\(1\le n,m\le 1000, 1\le q\le 200000, 1\le x_1\le x_2\le n, 1\le y_1\le y_2\le m, −1000\le矩阵内元素的值\le 1000\)

输入样例:

3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4

输出样例:

17
27
21
#include<iostream>
#include<cstdio>

using namespace std;

const int N = 1010;
int n, m, q;
int a[N][N], s[N][N];

int main() { 
    scanf("%d%d%d", &n, &m, &q);
    for(int i = 1; i <= n; i++) { 
        for(int j = 1; j <= m; j++) { 
            scanf("%d", &a[i][j]);
             //下式用于求矩阵的前缀和
            s[i][j] = s[i - 1][j] + s[i][j - 1] + a[i][j] - s[i - 1][j - 1];
        }
    }
    
    while(q--) { 
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        // 下式用于计算矩阵左下角(x1, y1)到矩阵右上角(x2, y2)之间元素的和
        printf("%d\n",s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]);
    }
    return 0;
}

差分

acwing-797 差分

题目

输入一个长度为 n 的整数序列。接下来输入 m 个操作,每个操作包含三个整数 l, r, c,表示将序列中 [l, r] 之间的每个数加上 c。请你输出进行完所有操作后的序列。

输入格式

第一行包含两个整数 n 和 m。第二行包含 n 个整数,表示整数序列。接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。

输出格式
共一行,包含 n 个整数,表示最终序列。

数据范围
\(1\le n,m\le 100000,1\le l\le r≤n,−1000\le c\le 1000,−1000\le 整数序列中元素的值\le 1000\)

输入样例:

6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1

输出样例:

3 4 5 3 4 2
#include <iostream>
using namespace std;

const int N = 1e5+10;
int n, m;
int a[N], b[N];

// 对a[l]~a[r]的值都加上c。 
void insert(int l, int r, int c){
	b[l] += c;
	b[r + 1] -= c; 
}

int main(){
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
        // 相当于在[i, i]区间内加上a[i]
		insert(i, i, a[i]);
	}
	
	while (m--) {
		int l, r, c;
		scanf("%d%d%d", &l, &r, &c);
		insert(l, r, c); 
	}
	
	for(int i = 1; i <= n; i++) {
		b[i] += b[i - 1];
		printf("%d ", b[i]);
	}
	
	return 0;	
}

acwing-798 差分矩阵

题目

输入一个n行m列的整数矩阵,再输入q个操作,每个操作包含五个整数\(x_1,y_1,x_2,y_2,c\),其中\((x_1, y_1)\)\((x_2, y_2)\)表示一个子矩阵的左上角坐标和右下角坐标。每个操作都要将选中的子矩阵中的每个元素的值加上c。请你将进行完所有操作后的矩阵输出。

输入格式
第一行包含整数n, m, q。接下来n行,每行包含m个整数,表示整数矩阵。接下来q行,每行包含5个整数\(x_1,y_1,x_2,y_2,c\),表示一个操作。

输出格式
共n行,每行m个整数,表示所有操作进行完毕后的最终矩阵。

数据范围
\(1\le n, m\le1000,1\le q\le 100000,1\le x_1\le x_2 \le n,1\le y_1\le y_2 \le m,-1000\le c\le 1000,-1000\le 矩阵内元素的值\le 1000\)

输入样例:

3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1

输出样例:

2 3 4 1
4 3 4 1
2 2 2 2
#include <iostream>
using namespace std;

const int N = 1e4 + 10;
int n, m, q;
int a[N][N], b[N][N];

// 关键函数
void insert (int x1, int y1, int x2, int y2, int c) {
	b[x1][y1] += c;
	b[x2 + 1][y1] -= c;
	b[x1][y2 + 1] -= c;
	b[x2 + 1][y2 + 1] += c;
}

int main(){
	scanf("%d%d%d", &n, &m, &q);
	 
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			scanf("%d", &a[i][j]);
            // 在[i][j]~[i][j]区间内加上a[i][j]
			insert(i, j, i, j, a[i][j]);
		}
	}
	
	while (q--) {
		int x1, y1, x2, y2, c;
		scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
        // 为以(x1, y1)为左下角,(x2, y2)为右上角的子矩阵中所有元素都加c
		insert(x1, y1, x2, y2, c);   
	}
	
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
            // 求b矩阵的前缀和
			b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
			printf("%d ", b[i][j]);
		}
		cout << endl;
	}
	
	return 0;
}

双指针算法

// 双指针算法的做法
// 1. 
for (int i = 0, j = 0; i < n; i++) {
	while(j < i && check(i, j)) j++;
	// 每道题的具体逻辑
}

// 核心思想  O(n^2)
for (int i = 0; i < n; i++) {
    for (int j = 0; i < n; j++) {
        
    }
}

// 双指针算法可以应用某些性质将上述朴素算法优化到O(n)


// 例子:输入一个字符串,将字符串中每个单词输出来
#include <iostream>
#include <string.h>
using namespace std;

int main() {
	char str[1010];
	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;
}

acwing-799 最长连续不重复子序列

题目

给定一个长度为n的整数序列,请找出最长的不包含重复数字的连续区间,输出它的长度。

输入格式

第一行包含整数n。第二行包含n个整数(均在0~100000范围内),表示整数序列。

输出格式

共一行,包含一个整数,表示最长的不包含重复数字的连续子序列的长度。

数据范围

\(1 \le n \le 100000\)

输入样例:

5
1 2 2 3 5

输出样例:

3
image-20220810203022817

j代表它最远能到什么位置。j的移动具有单调性,j的值只会越来越大,即j只会往右走不会往左走。如上图所示,i往右走一格为,j要么不移动,要么往右移动。假设j会往左移动,\(j'\sim i'\)之间就拥有最大不重复子串,那么\(j'\sim i\)之间也是不重复的,因为它是\(j'\sim i'\)的子串。那么\(j'\)就不应该往前移动为\(j\),因为\(j\sim i\)的长度小于\(j' \sim i\),与事实不符,因此假设错误,j只能往右移动。

// 朴素做法(暴力做法)

for (int i = 0; i < n; i++) {
    for (int j = 0; j <= i; j++) {
        if (check(i, j)){ // 判断i~j中是否有重复的字符
            res = max(res, i - j + 1);
        }
    }
}

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int n;
int a[N], s[N];

int main() {
	scanf("%d", &n);
	for (int i = 0; i < n; i++) scanf("%d", &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);
	}
	
	printf("%d", res);
	return 0;
}
#include<iostream>
using namespace std;
const int N=100010;
int n;
int a[N],s[N]; //a数组存数据,b数组作为桶记录每个数字出现的次数 
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
      scanf("%d",&a[i]);
    
    int res=0;
    for(int i=0,j=0;i<n;i++) //双指针 
    {
        s[a[i]]++; //指向一个数,就把对应数的次数+1 
        
        while(s[a[i]]>1) //当次数>1时,说明当前区间有重复元素,就是a[i] 
        {
            s[a[j]]--; //把j对应位置的数删掉 
            j++;
        }
        
        res=max(res,i-j+1); //每次取不包含重复的数的连续区间的长度的最大值 
        
    }    
    printf("%d\n",res);
    return 0;
}

acwing-800 数组元素的目标和

题目

给定两个升序排序的有序数组A和B,以及一个目标值x。数组下标从0开始。请你求出满足A[i]+B[j]=x的数对(i, j)。数据保证有唯—解。

输入格式

第一行包含三个整数n,m,x,分别表示A的长度,B的长度以及目标值x。第二行包含n个整数,表示数组A。第三行包含m个整数,表示数组B。

输出格式
共一行,包含两个整数i和j.

数据范围
数组长度不超过\(10^5\)。同—数组内元素各不相同。\(1\le数组元素\le 10^9\)

输入样例:

4 5 6
1 2 4 7
3 4 6 8 9

输出样例:

1 1

image-20220829091229868

/*
如果按传统思路,两个数组都从左到右遍历,时间复杂度妥妥的O(M∗N)。性能太差!发现如果A[i]+B[j]要是大于目标值x后,继续向前长大i,j都是无意义的,可以优化。

如果固定i,遍历j,再加上步骤(1)的优化,是不是就可以了呢?其实也缺失了一些东西:比如a[1]+b[1]<x,a[1]+b[2]<x,a[1]+b[3]<x,这样一把长大一个,长的太慢了,性能也不高。

如果能一次走两个,是不是会快?那怎么能一次走两个呢?可以考虑一个从左侧出发,另一个从右侧出发。是不是有点快速排序的意思?双指针,两边出发。

总结:
1. 对向而行,和大了就j−−,和小了就i++。
2. j具有单调性,i从a[0]到a[n],j从b[m - 1]到b[0],当a[i]+b[j]<x时,i向右移,a[i]变大,此时如果a[i]+b[j]>x,j应当左移。
3. j要么往左移动,要么不移动。
*/

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int a[N], b[N];

int main()
{
    int n, m, x;
    scanf("%d %d %d", &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 (j >= 0 && a[i] + b[j] > x)   j --;
        if (a[i] + b[j] == x) {
            printf("%d %d", i, j);
            break;
        }
    }
    return 0;
}

位运算

/*
操作一:求x的二进制表示中第k位数是多少。
	1. 先把x二进制表示的第k位移到最后一位   x >> k
	2. 看移动后的个位是什么   x & 1
操作二:lowbit(x):返回x的最后一位1
	lowbit(x) = x & -x
	例如:x=10100  lowbit(x)=100
		 x=111010 lowbit(x)=10
		 x=10101  lowbit(x)=1
		 x=101000 lowbit(x)=1000
*/

acwing-801 二进制中1的个数

题目

给定一个长度为n的数列,请你求出数列中每个数的二进制表示中1的个数。

输入格式

第一行包含整数n。第二行包含n个整数,表示整个数列。

输出格式

共一行,包含n个整数,其中的第i个数表示数列中的第i个数的二进制表示中1的个数。

数据范围

\(1\le n\le 100000, 0\le 数列中元素的值\le 10^9\)

输入样例:

5
1 2 3 4 5

输出样例:

1 1 2 1 2
#include<iostream>
using namespace std;
const int N = 1e5+10;
int a[N]; 
int n;

// 快速求得 x 二进制表示中最低位 1 表示的值(用来取一个二进制最低位的一与后边的0组成的数)
// 例如:12(1100),-12(0100),lowbit(12)的返回值为100
int lowbit(int x) {
    return x & (-x);
}

int main() {
    cin >> n;
    for(int i = 0; i < n; i++) {
        cin >> a[i];
        int res = 0;
        while(a[i]) {
            a[i] -= lowbit(a[i]);
            res ++;
        }
        cout << res << ' ';
    }
    cout << endl;
    return 0;
}

离散化

离散化,把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。一些题目的数据跨度很大,用按顺序来存储这些数很浪费空间,有时数组容量还不够。例如,一些题目需要处理数轴上很大范围内的小部分数据,这时我们如果用数组来存储数轴上这些数会很浪费时间,有时甚至会超出数组的界限,因此我们需要把数轴上这些数按大小顺序映射到一个较小的数据范围中。

我们可能有一些数据,这些数据的值域比较大(\(0 \sim 10^9\)),但数据的个数比较小(\(0\sim 10^5\))。有些题目我们需要把这些数据当作下标,如果我们将这些数据表示为数组的下标,那就需要开辟一个很大的数组,有可能我们甚至无法开辟这么大的数组。此时我们就需要进行离散化。

/*
假设有数组a{1,3,100,2000,10^9},离散化就会将他们转换为{0,1,2,3,4,5}
离散化中有两个问题
1. a中可能有重复元素(去重)。   v.erase(unique(v.begin(),v.end()),v.end());
2. 如何计算出原数组中每个数离散化后的值(用二分找)。

*/

acwing-802 区间和

题目

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

输入格式

第一行包含两个整数n和m。接下来 n 行,每行包含两个整数x和c。再接下里 m 行,每行包含两个整数l和r。

输出格式

共m行,每行输出一个询问中所求的区间内数字和。

数据范围

\(−10^9\le x\le 10^9,1\le n,m\le10^5,−10^9\le l\le r\le 10^9,−10000\le c\le 10000\)

输入样例:

3 3
1 2
3 6
7 5
1 3
4 6
7 8

输出样例:

8
0
5

如果数据范围比较小,我们可以直接用前缀和模板来做这个题目。

首先我们需要把该题中用到的与坐标有关的数据进行离散化。在该题中,总共有\(n+2m\)个与坐标有关的数据。n和m的最大为\(10^5\),那么存储与坐标相关的数组的大小为\(3\times 10^5\)

#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;
const int N=300010;
int n,m;
int a[N],s[N];

//存储所有待离散化的数据
vector<int> alls;
typedef pair<int,int> PII;
vector<PII> add, query; // add记录对x位置的操作,query记录两个下标值


/*
alls数组中下标从0开始,a和s数组下标从1开始 
*/

//二分求出x对应的离散化的值
// 找小于等于x的最大值
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 l;//映射到0,1,2,3,...
    return l+1; //映射到1,2,3,...
    
    // find函数也可以是以下写法
    // return lower_bound(alls.begin(), alls.end(), x) - alls.begin() + 1;
}
int main() {
	cin>>n>>m;
	for(int i=0;i<n;i++) {
		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);
	}
	//将所有值排序
	sort(alls.begin(),alls.end());
	//去掉重复元素
	alls.erase(unique(alls.begin(),alls.end()),alls.end());
	
	for(auto item: add) {
		int x=find(item.first);
		// 将alls数组中的对x位置的操作映射到a数组中去 
		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);
		int r=find(item.second);
		cout<<s[r]-s[l-1]<<endl;
	}
	return 0;
}

区间合并

acwing-803 区间合并

image-20220810222745991

最后区间合并为三个

题目

给定n个区间\([l_i, r_i]\),要求合并所有有交集的区间。注意如果在端点处相交,也算有交集。输出合并完成后的区间个数。例如:[1,3]和[2,6]可以合并为一个区间[1,6]。

输入格式

第一行包含整数n。接下来n行,每行包含两个整数l和r。

输出格式

共一行,包含一个整数,表示合并区间完成后的区间个数。

数据范围

\(1\le n\le 100000,-10^9\le l_i\le r_i\le 10^9\)

输入样例:

5
1 2
2 4
5 6
7 8
7 9

输出样例:

3

yxc版代码:

#include <iostream>
#include <algorithm>

using namespace std;
typedef pair<int, int> PII;
const int N = 100010;
int n;
vector<PII> segs;

void merge(vector<PII> &segs){
    vector<PII> res;//存答案
    //sort对pair是先排first,后排second的
    sort(segs.begin(), segs.end());
    //st和ed用于维护一段区间
    //这里其实就是:往后探一眼,然后确定分割再push当下,else否则更新
    //所以在循环出去之后要把最后一次push进去.
    //在开始要把st和ed赋值为很小,但是第一次"向后看"到的是第一个区间,故肯定不会push
    //push之前要确保st和ed不是初值!!!!!!
    int st = -2e9, ed = -2e9;
    for (auto seg : segs){
        if (ed < seg.first){
            if (st != -2e9) res.push_back({st, ed});
            st = seg.first, ed = seg.second;
        }
        else ed = max(ed, seg.second);
    }
    //最后一次也要输出,if是用于防止输入为空
    if (st != -2e9) res.push_back({st, ed});
    
    segs = res;
}

int main(){
    cin >> n;
    
    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;
}

简洁版代码:

#include <bits/stdc++.h>
using namespace std;
int n, cnt = 0;
struct S {
    int l, r;
} a[100010], ans[100010];

int cmp(S a, S b) {
    if (a.l == b.l) return a.r < b.r;
    return a.l < b.l;
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d%d", &a[i].l, &a[i].r);
    sort(a + 1, a + 1 + n, cmp);
    ans[++cnt] = a[1];
    for (int i = 2; i <= n; i++) {
        if (a[i].l <= ans[cnt].r) ans[cnt].r = max(ans[cnt].r, a[i].r);
        else ans[++cnt] = a[i];
    }
    cout << cnt << endl;
    return 0;
}
posted @ 2023-06-03 10:47  Omega丶  阅读(41)  评论(0)    收藏  举报