学习方法
- 课上:学习算法思想,明白这个算法为什么是对的,听课的时候跟着敲一遍代码,学完一个知识点之后,再独立敲一遍
- 课后:跟着博客进行复习
读入规模较大的时候,使用scanf
——积跬步,至千里;积小流,成江海。
快速排序
算法思想:基于分治
- 确定分界点
- 调整范围,使得分界点的左边元素都比其小,分界点的右边元素都比其大
- 递归处理左右两端
分治算法
题目链接:785. 快速排序
#include<iostream>
using namespace std;
const int N = 100005;
int 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){
//从左往右,找到大于x的数字
do{
i ++;
}while(q[i] < x);
//从右往左,找到小于x的数字
do{
j --;
}while(q[j] > x);
//将大于x的元素和小于x的元素进行交换
if(i < j){
int t = q[i];
q[i] = q[j];
q[j] = t;
}
}
//x左边的元素都小于等于它,x右边的元素都大于等于它
//递归处理x左右两边
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
int main(){
int n;
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]);
}
return 0;
}
题目链接:786. 第k个数
利用快速排序的思想解题,每次划分,只要递归包含第k个数的那部分,时间复杂度为O (n)
归并排序
归并排序也是基于分治法:
- 分解:将数列分成左右两半
- 解决:递归排序左边和右边,分解到不能再分解
- 合并:把每次分开的两部分合并到一起
#include<iostream>
using namespace std;
const int N = 100005;
int q[N], tmp[N];//tmp为辅助数组
void merge_sort(int q[], int l, int r){
if(l >= r){//如果区间个数为1个或者没有的话,直接返回
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(int i = l, j = 0; i <= r; i++, j ++){
q[i] = tmp[j];
}
}
int main(){
int n;
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]);
}
return 0;
}
二分
整数二分:
二分的本质其实是“边界”,例如,左边红色区域可以满足某个条件,右边绿色区域不可以满足某个条件,注意两个区间是没有交集的,因为这是整数二分,那么二分就是找到这个边界,那么既可以是红色区域的最右边那个位置,也可以是绿色区域最左边那个位置,这就对应着接下来将的两个模版。
二分一定有解,无界是题目中涉及的情况,可以特判。
题目链接:789. 数的范围
#include<iostream>
using namespace std;
const int N = 100005;
int q[N];
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i ++){
cin >> q[i];
}
while(m--){
int x;
scanf("%d", &x);
//先递归满足条件的最左边
int l = 0, r = n - 1;
while(l < r){
int mid = (l + r) >> 1;
if(q[mid] >= x){// 边界的位置在左半边
r = mid;// 这种更新条件, mid不需要补1
}else{
l = mid + 1;
}
}
if(q[l] != x){
cout << "-1 -1"<< endl;
}else{
cout << l << ' ';
//递对满足条件的右边界
int l = 0, r = n - 1;
while(l < r){
int mid = 1 + (l + r) >> 1;
if(q[mid] <= x){
l = mid;
}else{
r = mid - 1;
}
}
cout << l <<' ' << endl;
}
}
return 0;
}
实数二分
题目链接:790. 数的三次方根
#include<iostream>
using namespace std;
int main(){
double n;
cin >> n;
double l = -100, r = 100;// 三次方根的范围
while(r - l > 1e-8){// 当区间长度小于一个很小的数字的时候就得到结果
double mid =(l + r) / 2;
if(mid * mid * mid >= n){
r = mid;
}else{
l = mid;
}
}
printf("%.6lf\n", l);
return 0;
}
高精度
高精度情况分类
- A+B:一个大整数加上一个大整数 1 0 6 10^6 106
- A-B:一个大整数-一个大整数 1 0 6 10^6 106
- A*b:一个大整数 * 一个小整数
高精度加法
题目链接:791. 高精度加法
#include<iostream>
#include<vector>
using namespace std;
//C = A + B
vector<int> add(vector<int> &A, vector<int> &B){
vector<int> C;
int t = 0;// 用来存储进位
for(int i = 0; i < A.size() || i < B.size(); i ++){
if(i < A.size()) t += A[i];
if(i < B.size()) t +=B[i];
C.push_back(t % 10);
t /= 10;
}
if(t){
C.push_back(1);
}
return C;
}
int main(){
string a, b;
vector<int>A, B;
cin >> a >> b;// a = "123456"
for(int i = a.size() - 1; i >= 0; i--){ //A = [6, 5, 4, 3, 2, 1]
A.push_back(a[i] - '0');
}
for(int i = b.size() - 1; i >= 0; i--){
B.push_back(b[i] - '0');
}
auto c = add(A, B);
for(int i = c.size() - 1; i >= 0; i --){
printf("%d", c[i]);
}
return 0;
}
高精度减法
题目链接:792. 高精度减法
#include<iostream>
#include<vector>
using namespace std;
//判断是否有 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;
}
//C = A - B
vector<int> sub(vector<int> &A, vector<int> &B){
vector<int> C;
for(int i = 0, t = 0; i < A.size(); i++){//已经判断过A和B的大小,所以A.size() >= B.size()
t = A[i] - t;//减去借位
if(i < B.size()){
t -= B[i];
}
C.push_back((t + 10) % 10);
if(t < 0){
t = 1;
}else{
t = 0;
}
}
//去掉前导0
while(C.size() > 1 && C.back() == 0){
C.pop_back();
}
return C;
}
int main(){
string a, b;
cin >> a >> b;
vector<int> A, B;
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');
}
//判断两个大整数,谁大
if(cmp(A, B)){
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("-");//-(B - A)
for(int i = C.size() - 1; i >= 0; i--){
printf("%d", C[i]);
}
}
return 0;
}
高精度乘法
题目链接:793. 高精度乘法
#include<iostream>
#include<vector>
using namespace std;
vector<int> mul(vector<int> &A, int b){
vector<int> C;
int t = 0;// 第零位进位为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;
}
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');
}
if(b == 0){
printf("0");
}else{
auto C = mul(A, b);
for(int i = C.size() - 1; i >= 0; i --){
printf("%d", C[i]);
}
}
return 0;
}
高精度除法
题目链接:794. 高精度除法
#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);
r %= b;
}
reverse(C.begin(),C.end());
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');
}
int r;
auto C = div(A, b, r);
for(int i = C.size() - 1; i >= 0; i--){
printf("%d", C[i]);
}
cout << endl;
cout << r << endl;
return 0;
}
1h: 18min
前缀和和差分
一维前缀和
数组元素(这里规定下标从1开始,a[0] = 0,下文解释): a 1 + a 2 + a 3 . . . + a n a_1 + a_2 + a_3 ...+ a_n a1+a2+a3...+an
前缀和公式: S i S_i Si = a 1 + a 2 + a 3 . . . + a i a_1 + a_2 + a_3 ...+ a_i a1+a2+a3...+ai
如何求 S i S_i Si
s[0] = 0;//下标从1开始,a[0] = 0;
for(int i = 1; i <= n; i++){
s[i] = s[i - 1] + a[i];
}
前缀和的作用
例如:计算 a l + a l + 1 + a l + 2 . . . + a r a_l+ a_ {l + 1}+ a_{l + 2} ...+ a_{r} al+al+1+al+2...+ar,就是求集合中[l, r]元素的和
使用前缀和的解决方式:s[r] - s[l -1] =( a 1 + a 2 + a 3 . . . + a r a_1 + a_2 + a_3 ...+ a_r a1+a2+a3...+ar)- ( a 1 + a 2 + a 3 . . . + a l a_1 + a_2 + a_3 ...+ a_l a1+a2+a3...+al)= a l + a l + 1 + a l + 2 . . . + a r a_l+ a_ {l + 1}+ a_{l + 2} ...+ a_{r} al+al+1+al+2...+ar
这样做的优点在于提高了速度,如果从l遍历到n进行计算,时间复杂度为O(n),而使用前缀和的方式,时间复杂度为O(1)
那么解释一下为什么元素下标从1开始呢,是为了处理边界 。由a[0] = 0,可以得出s[0] = 0。如果想要计算[1, 10],则计算s[10] - s[0],这里s[0] = 0,所以在边界情况就不需要特判了。
题目链接:前缀和
#include<iostream>
using namespace std;
const int N = 100005;
int a[N];
int s[N];
int main(){
int n, m, l, r;
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> a[i];//读入数组元素
s[i] =a[i] + s[i - 1];//更新前缀和数组
}
for(int i = 1; i <= m; i++){
cin >> l >> r;
cout << s[r] - s[l - 1] <<endl;//求【l,r】元素的和
}
return 0;
}
二维前缀和
二维前缀和是用来在一个矩阵中求子矩阵的和。例如我们用s[3][3]
表示下面这个绿色区域的元素总和。
(x1, y1) (x2, y2)这一子矩阵中所有数的和如何计算
s[i][j]
如何计算
用二维数组a存储矩阵中每一个元素,可以用以下方式计算s[i][j]
题目链接:子矩阵的和
#include<iostream>
using namespace std;
int a[1005][1005];
int s[1005][1005];
int main(){
int n, m, q;
int x1, y1, x2, y2;
cin >> n >> m >> q;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
cin >> a[i][j];
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
}
}
while(q --){
cin >> x1 >> y1 >> x2 >> y2;
printf("%d\n",s[x2][y2] - s[x1 -1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
}
return 0;
}
一维差分
《算法竞赛》P65-67
题目链接:797. 差分 - Acwing题库
#include<iostream>
using namespace std;
const int N = 100010;
int a[N], b[N];
void insert(int l, int r, int c){
b[l] += c;
b[r + 1] -= c;
}
int main(){
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> a[i];
}
for(int i = 1; i <=n; i ++){
insert(i, i, a[i]);
}
while(m --){
int l, r, c;
cin >> l >> r >> c;
insert(l, r, c);
}
//对差分数组求一遍前缀和
for(int i = 1; i <= n; i ++){
b[i] += b[i - 1];
cout << b[i] << " ";
}
return 0;
}
二维差分
a[i][j]是b[i][j](没有被表示出来)的前缀和
现在的目的就是在中间的子矩阵中加上一个c,如何做呢,可以按照下述四步。
#include <iostream>
using namespace std;
const int N = 1010;
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]);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
insert(i, j, i, j, a[i][j]);
while (q -- )
{
int x1, y1, x2, y2, c;
cin >> 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[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= m; j ++ ) printf("%d ", b[i][j]);
puts("");
}
return 0;
}
区间合并
803. 区间合并 - Acwing题库
解题思路:
输入的所有区间,看其中能否合并,能合并的则合并,合并之后算作一个区间,最后看总共包含几个区间。就是要思考如何快速的进行区间合并。
合并:
- 排序: 区间按照起点进行排序,确保可以按顺序处理。
- 合并逻辑:
- st 表示当前区间的起点,ed 表示当前区间的终点。
- 如果当前区间的起点大于当前正在合并的区间的终点 (ed < seg.first),则将之前的区间加入到结果中,并开始一个新的区间。否则,两个区间有重叠,此时更新终点为两个区间的最大终点 (ed = max(ed, seg.second))。
- 最后,将合并后的区间保存回 segs。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//这里定义了一个 PII 类型,表示一个包含两个整数的 pair(对),这两个整数表示区间的起点和终点
typedef pair<int, int> PII;
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end());
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 (st != -2e9) res.push_back({st, ed});
segs = res;
}
int main()
{
int n;
scanf("%d", &n);
vector<PII> segs;
for (int i = 0; i < n; i ++ )
{
int l, r;
scanf("%d%d", &l, &r);
segs.push_back({l, r});
}
merge(segs);
cout << segs.size() << endl;
return 0;
}