# 时间复杂度

# 排序

# sort

复杂度:O (nlogn)

1  #include<bits/stdc++.h>
 2  using namespace std;
 3  bool my_less(int i, int j)     {return (i < j);}  // 自定义小于函数
 4  bool my_greater(int i, int j)  {return (i > j);}  // 自定义大于函数
 5  int main (){
 6      int a[]={3,7,2,5,6,8,5,4};
 7      sort(a,a+4);                          // 对前 4 个数排序,结果:2 3 5 7 6 8 5 4
 8      for(int i=0;i<8;i++) cout<<a[i]<< “ “;cout<<”\n”;   // 下面可以复制这一行输出
 9      sort(a,a+8,less<int>());              // 从小到大排序,结果:2 3 4 5 5 6 7 8
10      sort(a,a+8,my_less);                  // 自定义排序,结果:2 3 4 5 5 6 7 8
11      sort(a,a+8,greater<int>());           // 从大到小排序,结果:8 7 6 5 5 4 3 2
12      sort(a,a+8,my_greater);               // 自定义排序,结果:8 7 6 5 5 4 3 2
13  
14      vector<int> c = {1,2,3,4,5,6,7,8};
15      sort(c.begin(),c.end(),my_greater);   // 结果:8 7 6 5 4 3 2 1
16      for(int i=0; i<c.size(); i++)  cout<<c[i]<< “ “;cout<<”\n”;
17  
18      string s=”hello world”;
19      sort(s.begin(),s.end());
20      cout<<s;                              // 输出:dehllloorw。注意第一个是空格
21      return 0;
22  }

# 题目:错误票据

注意:这里注意怎么读数据

这段代码使用了 scanf 函数来不断读取输入,直到遇到文件结束符(EOF)。在这里, scanf 函数尝试读取一个整数,并将其存储在 a[cnt] 中。如果成功读取一个整数,则 scanf 函数返回 1;如果遇到文件结束符,则 scanf 函数返回 EOF。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+10;
int a[N];
int main(){
    int n; cin >> n;
    int cnt = 0;
    while(scanf("%d", &a[cnt]) != EOF)   cnt++;    // 注意读数据的写法
    sort(a, a+cnt);
    int ans1, ans2;
    for(int i = 1; i < cnt; i++) {               
        if(a[i] - a[i-1] > 1)   ans1 = a[i-1]+1;   // 查找断号
        if(a[i] == a[i-1])      ans2 = a[i];       // 查找重号
    }
    cout << ans1 << ' ' << ans2;
    return 0;
}

# 题目:结构体排序

#include<bits/stdc++.h>
using namespace std;
struct stu{
    int id;      // 学号
    int c,m,e;   // 语文、数学、英语成绩
    int sum;
} st[305];
bool cmp(stu a, stu b){
    if(a.sum > b.sum) return true;
    else if(a.sum < b.sum) return false;
    else{                                 //a.sum == b.sum
        if(a.c > b.c) return true;
        else if(a.c < b.c) return false;
        else{                             //a.c == b.c
            if(a.id > b.id) return false;
            else return true;
        }
    }
}
int main(){
    int n;
    cin >> n;
    
    for(int i=1; i<=n; i++){
        st[i].id = i;                              // 学号
        cin >> st[i].c >> st[i].m >> st[i].e;
        st[i].sum = st[i].c + st[i].m + st[i].e;   // 总分
    }
    
    sort(st+1, st+1+n, cmp);
    
    for(int i=1; i<=5; i++)  
        cout << st[i].id << ' ' << st[i].sum << "\n";
        
    return 0;
}

# 排列和组合

# 全排列

STL 提供了求下一个排列组合的函数 next_permutation ()。例如对于由 3 个字符 {a,b, c} 组成的序列,next_permutation () 能按字典序返回 6 个组合:abc、acb、bac、bca、cab、cba。

返回值:如果没有下一个排列组合,则返回 False,否则返回 True。每执行一次 next_ permutation (),新的排列就会被放到原来的空间里。

✧ 提示:它排列的范围是 [first, last],包括 first,不包括 last。

next_permutation () 从当前的全排列开始,逐个输出更大的全排列,而不是输出所有的全排列。如果要得到所有的全排列,就需要从最小的全排列开始。

1  #include <bits/stdc++.h>
 2  using namespace std;
 3  int main(){
 4      string s=”bca”;
 5      sort(s.begin(),s.end());  // 字符串内部排序,得到最小的排列 “abc”
 6      do{
 7         cout<<s<<’ ‘;
 8      }while(next_permutation(s.begin(),s.end()));
 9      return 0;
10  }   // 输出:abc acb bac bca cab cba

# 题目:拼数

【题目描述】设有 n 个正整数 a1、a2……an,将它们连接成一排,相邻数字首尾相接,组成一个最大的整数。n≤20。【输入描述】第一行有一个整数,表示数字个数 n。第二行有 n 个整数。【输出描述】输出一个正整数,表示最大的整数。

暴力超时。

✧ 提示:这其实是按两个数字组合的字典序排序,也就是把数字看作字符串来排序,下面 C++ 代码第 4 行的 cmp () 函数体现了这一思路。

此算法的总复杂度等于库函数 sort () 的复杂度,为 O (nlogn)。

1  #include<bits/stdc++.h>
 2  using namespace std;
 3  string a[21];  // 记录 20 个数,用字符形式
 4  bool cmp (string a, string b){               // 从大到小,按字典序的反序排列
 5      return a + b > b + a;                    // 组合字符串,注意这个技巧
 6  }
 7  int main( ){
 8      int n;    cin >> n;
 9      for(int i=0; i<n; i++)   cin >> a[i];
10      sort(a, a+n, cmp);                       // 从大到小,按字典序的反序排列
11      for(int i=0; i<n; i++)     cout << a[i];
12      return 0;
13  }

# 题目:带分数

【题目描述】100 可以表示为带分数的形式:3 + 69258 / 714。还可以表示为:82 + 3546 / 197。注意特征:带分数中,数字 1~9 分别出现且只出现一次(不包含 0)。类似这样的带分数,100 有 11 种表示法。输入一个整数,输出它有多少种带分数表示法。【输入描述】从标准输入读入一个正整数 N (N < 1000000)。【输出描述】程序输出该数字用数字 1~9 不重复、不遗漏地组成带分数表示的全部种数。

n = a + b/c 移项得 ac + b = nc,在第 18 行判断是否符合此式

1  #include<bits/stdc++.h>
 2  using namespace std;
 3  int num[9]={1,2,3,4,5,6,7,8,9};
 4  int check(int L,int R){       // 计算数字
 5      int res=0;
 6      for(int i=L;i<=R;i++)   res=res*10+num[i];
 7      return res;
 8  }
 9  int main(){
10      int n; cin>>n;
11      int cnt=0;
12      while(next_permutation(num,num+9)){
13          for(int i=0;i<7;i++){
14              for(int j=i+1;j<8;j++){
15                  int a = check(0,i);
16                  int b = check(i+1,j);
17                  int c = check(j+1,8);
18                  if(a*c+b == c*n)  cnt++;
19              }
20          }
21      }
22      cout<<cnt;
23      return 0;
24  }

# 尺取法

尺取法可以把两重循环转化为一重循环,从而把复杂度从 O (n2) 变成 O (n)。

✧ 提示:用尺取法的最关键之处在于,两个指针 i、j 在总体上只能有一个循环,i 循环一遍,对应的 j 只能跟随 i 循环一遍。这样才能实现计算复杂度从 O (n2) 到 O (n) 的优化。所以尺取法的应用有很大局限。

1  for (int i = 0, j = n - 1; i < j; i++, j--) 
 2  {  ... }
1  // 用 while 循环实现尺取法
 2  int i = 0, j = n - 1;
 3  while (i < j) {      //i 和 j 在中间相遇,并且要防止 i、j 越界
 4          ...          // 满足题意的操作
 5          i++;         //i 从头扫描到尾
 6          j--;         //j 从尾扫描到头
 7  }

# 反向扫描

# 题目:回文

【题目描述】给定一个长度为 n 的字符串 S。请你判断字符串 S 是否回文。【输入描述】输入仅一行,包含一个字符串 S。1≤|S|≤106,保证 S 只包含大小写字母。【输出描述】若字符串 S 回文,则输出 “Y”,否则输出 “N”。

1  #include <bits/stdc++.h>                     #include <bits/stdc++.h>
 2  using namespace std;                         using namespace std;
 3  int main(){                                  int main(){
 4      string s;  cin >> s;    // 读字符串            string s;  cin >> s;      // 读字符串
 5      int n = s.size();                            int n = s.size();
 6                                                   int i = 0, j = n-1;       // 双指针
 7      for(int i=0,j=n-1;i<j;i++,j--)// 双指针        while (i < j){
 8          if(s[i] != s[j]) {                           if(s[i] != s[j]){cout<<’N’; return 0;}
 9              cout<<’N’; return 0;                     i++, j--;             // 移动双指针
10          }                                        }
11      cout<<’Y’;                                   cout << ‘Y’ ;
12      return 0;                                    return 0;
13  }                                            }

# 同向扫描

同向扫描的题目和 “滑动窗口” 有关,指针 i 和 j 之间的区间,随着 i 和 j 向前扫描,形成了一个滑动窗口,借助这个滑动窗口能遍历和计算序列上的区间问题。

# 题目:美丽区间

【题目描述】给定一个长度为 n 的序列 a1, a2,…, an 和一个常数 S。对于一个连续区间,如果它的区间和大于或等于 S,则称它为美丽的区间。对于一个美丽的区间,其区间长度越短,它就越美丽。请你从序列中找出最美丽的区间。

【输入描述】第一行输入两个整数 n、S,其含义如题所述。接下来的一行输入 n 个整数,分别表示 a1、a2……an 。10≤N≤105,1≤ai≤104,1≤S≤108。

【输出描述】输出共一行,包含一个整数,表示最美丽的区间的长度。若不存在任何美丽的区间,则输出 0。

“滑动窗口”,求窗口内的区间和大于 S 的最小区间长度,复杂度为 O (n)。

1  #include <bits/stdc++.h>
 2  using namespace std;
 3  int a[100010];
 4  int main(){
 5    int n, S;  cin>>n>>S;
 6    for (int i=0; i<n; ++i) cin>>a[i];
 7    int sum= 0, ans=1e8;
 8    for (int i=0, j=0; i<n;) {
 9      if(sum<S){ sum+=a[i]; i++;}
10      else     { ans=min(i-j,ans); sum-=a[j]; j++; }
11    }
12    if (ans==1e8) cout<<0;
13    else          cout<<ans;
14    return 0;
15  }

# 题目:日志统计

(1) 定义 i 指针,它对应主循环,遍历随时间而失效的所有帖子。

(2) 定义 j 指针,其作用是在时刻 i = T,把 [T−D, T] 之前的帖子都置为无效

总复杂度也是 O (nlogn)。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+50;
int num[N]; //num [i]:记录 id=i 的帖子的 “赞” 的数量
int flag[N]; //flag [i]:id=i 的帖子曾是热帖
struct post{int id, ts;} p[N]; // 记录帖子
int cmp(post x, post y) {
    return x.ts < y.ts;
}
int main(){
    int n, d, k;
    cin >> n >> d >> k;
    
    for(int i = 0; i < n; i++) {
        scanf("%d%d", &p[i].ts, &p[i].id);
    }
    sort(p, p + n, cmp); // 按时间从小到大排序
    for(int i = 0, j = 0; i < n; i++){
        num[p[i].id]++;
        while(p[i].ts - p[j].ts >= d){
            num[p[j].id]--; // 随着时间流逝,d 之前的每个帖子的点赞数都减 1
            j++;
        }
        if(num[p[i].id] >= k) {
            flag[p[i].id] = 1; // 在 [i-d,i] 上达到 k 个 “赞”
        }
    }
    for(int i = 0; i < N; i++) {
        if(flag[i] == 1) {
            printf("%d\n", i);
        }
    }
    return 0;
}

# 二分

二分法可以把一个长度为 n 的有序序列上 O (n) 的查找时间优化到 O (logn)。

二分法的前提:序列是有序的,按从小到大或从大到小排序。

# 二分模板

1  #include<bits/stdc++.h>
 2  using namespace std;
 3  int a[1000];
 4  int bin_search(int *a, int n, int x){    // 在数组 a 中查找数字 x,返回位置
 5      int left = 0, right = n;
 6      while (left < right) {
 7          int mid = left+(right-left)/2;
 8          if (a[mid] >= x) right = mid;
 9          else             left = mid+1;
10          cout<<a[mid]<< “ “;              // 输出猜数的过程
11      }
12     return left;
13  }
14  int main(){
15      int n = 100;
16      for(int i=0;i<n;i++) a[i]=i+1;      // 赋值,数字 1~100
17      int test = 54;                      // 猜 54 这个数
18      int pos = bin_search(a,n,test);
19      cout<<”\n”<<”test=<<a[pos];
20  }

# 整数二分

# 在单调递增序列中查找 x 或者 x 的后继

在单调递增数列 a [] 中查找某个数 x,如果数列中没有 x,则查找比它大的下一个数

1  #include<bits/stdc++.h>
 2  using namespace std;
 3  int a[1000];
 4  int bin_search(int *a, int n, int x){     //a [0]~a [n-1] 是单调递增的
 5      int left = 0, right = n;              // 注意:不是 n-1,因为此时是左闭右开的区间 [0,n)
 6      while (left < right) {
 7          int mid = left + (right-left)/2;  //int mid = (left + right) >> 1;
 8          if (a[mid] >= x)  right = mid;
 9          else    left = mid + 1;
10      }                                     // 终止于 left = right
11     return left;
12  }
13  int main(){
14      int n = 100;
15      for(int i=0;i<n;i++) a[i]=2*i+2;      // 赋值,数字 2~200,偶数
16      int test = 55;                        // 查找 55 或 55 的后继
17      int pos = bin_search(a,n,test);
18      cout<<”test=<<a[pos];              
19  }

# 在单调递增序列中查找 x 或者 x 的前驱

1  #include<bits/stdc++.h>
 2  using namespace std;
 3  int a[1000];
 4  int bin_search2(int *a, int n, int x){    //a [0]~a [n-1] 是单调递增的
 5      int left = 0, right = n;
 6      while (left < right) {
 7          int mid = left + (right-left + 1)/2 ;
 8          if (a[mid] <= x)  left = mid;
 9          else  right = mid - 1;
10      }                                     // 终止于 left = right
11     return left;
12  }
13  int main(){
14      int n = 100;
15      for(int i=0;i<n;i++) a[i]=2*i+2;     // 赋值,数字 2~200, 偶数
16      int test = 55;                       // 查找 55 或 55 的前驱
17      int pos = bin_search2(a,n,test);
18      cout<<”test=<<a[pos];
19  }

# 题目:巧克力

一个一个试边长 d 太慢了,现在使用二分法

用暴力法需要执行 d 次 check (),用二分法只需要执行 O (logd) 次 check ()

#include<bits/stdc++.h>
using namespace std;
int n, k;
const int N = 100010;
int h[N], w[N];
bool check(int d){
    int num = 0;
    for(int i=0; i<n; i++)  
        num += (h[i] / d) * (w[i] / d);
    if(num >= k) 
        return true;      // 够分
    else       
        return false;     // 不够分
}
int main(){
    cin >> n >> k;
    for(int i=0; i<n; i++)   
        cin >> h[i] >> w[i];
    int L = 1, R = N;                //R 的初值是 100010
    // 第一种写法:
    while(L < R) {
        int mid = (L + R + 1) >> 1;      // 除以 2,向右取整
        if(check(mid))  
            L = mid;   // 新的搜索区间是右半部分,R 不变,调整 L=mid
        else            
            R = mid - 1; // 新的搜索区间是左半部分,L 不变,调整 R=mid–1
    }
    cout << L;
    // 第二种写法:
    /*  while (L<R) {
        int mid=(L+R)>>1;        // 除以 2,向左取整
        if (check (mid)) L=mid+1;  // 新的搜索区间是右半部分,R 不变,更新 L=mid+1
        else           R=mid;    // 新的搜索区间是左半部分,L 不变,更新 R=mid
    }
    cout << L-1;    */
    return 0;
}

# 跳石头 (最小值最大化)

这是一道二分法应用的套路题:“最小值最大化”。类似的套路题还有 “最大值最小化”。

在 n 块岩石中移走 m 块岩石,有很多种移动方法。在第 i 种移动方法中,剩下的石头之间的距离,有一个最小距离 ai。所有移动方法的最小距离 ai 中,问最大的 ai 是多少。在所有可能的最小值中查找最大的那个值,就是 “最小值最大化”

不去找搬走岩石的各种组合,而是给出一个距离 d,检查能不能搬走 m 块岩石而得到最短距离 d。把所有的 d 都试一遍,肯定能找到一个最短的 d。用二分法找这个 d 即可。


# 题目:

# 题目:

# 实数二分

1  const double eps = 1e-7;                    // 精度。如果用 for 循环,可以不要 eps
 2  while(right - left > eps){                  //for(int i = 0; i<100; i++){
 3        double mid = left+(right-left)/2;       
 4        if (check(mid)) right = mid;          // 判定,然后继续二分
 5        else            left  = mid;
 6  }

(1) 使用 while 循环的实数二分。第 1 行的极小数字 eps 用于判断 [left, right] 是不是足够小,如果 left 和 right 相差小于 eps,则认为 left 和 right 是相等的。eps 需要根据题目来设定。eps 实际上决定了二分的次数。例如区间长度为 1,要达到 eps = 0.001 的精度,做 10 次二分就能实现 (1/210 = 0.001)。第 2 行用 while 循环判断 eps,过小的 eps 会导致超时,过大的 eps 会导致出错。

(2) 使用 for 循环的实数二分。其实不用 eps 也行,直接进行足够多的二分就好了。第 2 行用 for 循环实现,二分 100 次,最后的 [left, right] 长度是初始区间长度的 1/2100,得到的精度比任何 eps 都小,right 与 left 的差值已经小得不能再小了。一般循环 100 次,不过有时候因为题目的逻辑比较复杂,一次 for 循环内部的计算量很大,所以较大的 for 循环次数会导致超时,此时应把 100 次减少到 50 次甚至更少。

# 题目:一元三次方程求解

根与根之差的绝对值大于或等于 1。那么在每个 [i, i+1] 的小区间内做二分查找就行了

#include <bits/stdc++.h>
using namespace std;
double a, b, c, d;
double y(double x){ 
    return a * x * x * x + b * x * x + c * x + d;
}
int main(){
    scanf("%lf%lf%lf%lf", &a, &b, &c, &d);  // 输入
    for (int i = -100; i < 100; i++){     // 题目说 “根与根之差的绝对值大于或等于 1”,分为 200 个小区间
        double left = i, right = i + 1;
        double y1 = y(left), y2 = y(right);
        if(y1 == 0) 
            printf("%.2lf ", left); // 判断左端点。一个小坑点,容易被忽略
        if(y1 * y2 < 0){                     // 小区间内有根
            for(int j = 0; j < 100; j++){     // 在小区间内二分
                double mid = (left + right) / 2;
                if(y(mid) * y(right) <= 0)
                    left = mid;
                else
                    right = mid;
            }
            printf("%.2lf ", right);
        }
    }
    return 0;
}

# 倍增法和 ST 算法

倍增法和二分法是 “相反” 的算法。二分法每次缩小为上一次的 1/2,倍增法每次扩大一倍,两者都以 2 的指数减少或增加,效率极高。

倍增法是扩大区间,例如在大区间上求解和区间查询有关的问题,求区间最大值或最小值。

✧ 提示:倍增法也有局限性,就是需要提前计算出第 1、2、4……2k 个跳板,这要求数据是静态不变的,而不是动态变化的。如果数据发生了变化,那么所有跳板都得重新计算。如果要反复重新计算跳板,倍增法就失去了意义。

# 暴力解决区间问题

静态空间的 RMQ:给定长度为 n 的静态数列,做 m 次询问,每次给定 L, R≤n,查询 [L, R] 内的最值。

1  #include <bits/stdc++.h>
 2  using namespace std;
 3  int main(){
 4      int a[7] = {5,7,1,2,3,6,9};
 5      int id = min_element(a+2, a+5) - a;   // 注意,区间 [a+2, a+5) 左闭右开,不包括 a+5
 6      cout<<a[id]<< “\n”;                   // 输出最小值 1
 7      id = max_element(a+2, a+5) - a;
 8      cout<<a[id]<< “\n”;                   // 输出最大值 3
 9      int s = accumulate(a+2, a+5,0);       // 最后的 0 表示从 0 开始累加
10      cout<<s<<”\n”;                        // 输出区间和 6
11      return 0;
12  }

# ST 算法

ST 算法是求解 RMQ 的高效算法,适用于静态空间的 RMQ 查询。ST 算法做一次区间查询的复杂度是 O (1),m 次查询的总复杂度只有 O (m)。

ST 算法基于一个简单原理:一个大区间若能被两个小区间覆盖,则大区间的最值可以用两个小区间的最值计算出来。

1  #include <bits/stdc++.h>
 2  using namespace std;
 3  const int N = 500001;
 4  int n,m;
 5  int a[N],dp[N][40];                    
 6  void st_init(){
 7      for(int i=1;i<=n;i++)  dp[i][0] = a[i];         // 初始化区间长度为 1 时的值
 8      int p = (int)(log(double(n)) / log(2.0));       //int p=log2 (n);  // 此处有两种写法
 9      for(int k=1;k<=p;k++)                                     // 用 dp [] 递推计算区间
10          for(int s=1;s+(1<<k)<=n+1;s++)
11              dp[s][k]=max(dp[s][k−1], dp[s+(1<<(k-1))][k-1]);  // 最大值
12  }
13  int st_query(int L,int R){
14      int k = (int)(log(double(R-L+1)) / log(2.0));  //int k = log2 (R-L+1); // 两种方法求 k
15      return max(dp[L][k],dp[R-(1<<k)+1][k]);        // 最大值
16  }
17  int main(){
18      scanf(%d%d”,&n,&m);
19      for(int i=1;i<=n;i++)  scanf(%d”,&a[i]);
20      st_init();
21      for(int i=1;i<=m;i++){
22          int L,R; scanf(%d%d”,&L,&R);
23          printf(%d\n”,st_query(L,R));
24      }
25      return 0;
26  }

# 前缀和

一个长度为 n 的数组 a [0]~a [n-1],它的前缀和 sum [i] 等于 a [0] ~a [i] 的和。

利用递推,只需 n 次就能计算出所有的前缀和:sum [i] = sum [i−1] + a [i]。也能用 sum [] 反推计算出 a []:a [i] = sum [i]−sum [i−1]。

快速计算出数组中任意一个区间 a [i]~a [j] 的和:a [i] + a [i+1] + … + a [j−1] + a [j] = sum [j]− sum [i−1]。

这个式子的左边用暴力法计算,计算复杂度是 O (n),右边用前缀和计算,计算复杂度是 O (1)。这就是前缀和的优势。

# 题目:矩阵

#include<bits/stdc++.h>
using namespace std;
int s[550][550];
int main(){
    int n,m,k; cin>>n>>m>>k;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++) {
            int a;   scanf("%d",&a);
            s[i][j] = s[i-1][j] + a;            //s [i][j]:第 j 列上,第 0~i 行数字的前缀和
        }
    long long ans=0;
    for(int i1=1;i1<=n;i1++)
        for(int i2=i1;i2<=n;i2++)
            for(int j1=1,j2=1,z=0;j2<=m;j2++){  // 尺取法,滑动窗口为 [j1,j2]。移动指针 j2
                z += s[i2][j2]-s[i1-1][j2];     // 第 j2 列上,i1、i2 的区间和。累加得到二维区间和
                while(z>k){                     // 若区间和 > k,则移动指针 j1
                    z -= s[i2][j1]-s[i1-1][j1];
                    j1 += 1;                  
                }
                ans += j2-j1+1;         // 若 j1、j2 的区间和 < k,则这里面有 j2-j1+1 个满足条件
            }
    cout << ans;
    return 0;
}

# 贪心

✧ 提示:当一道题目的解题过程是从局部推算到全局时,若使用贪心算法不能求得最优解,则一般能用 DP 求得最优解。任意面值的最少硬币支付问题,其正解是 DP。

如何判断一道题目能否用贪心算法求解?要用贪心算法求解的问题,需要满足以下特征。

(1) 最优子结构性质。当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质,也称此问题满足最优性原理。也就是说,从局部最优能扩展到全局最优。

(2) 贪心选择性质。问题的整体最优解可以通过一系列局部最优的选择来得到。

更新于