--- title: "01-根据时间复杂度选择算法" created: 2026-01-09 tags: - 博客 --- # 根据时间复杂度选择算法 ![[Pasted image 20260907191939.png]] ## 总览 1. n≤30 → 指数级别 dfs + 剪枝,数字排列, n皇后问题, 八数码问题 状态压缩 dp,蒙德里安的梦想, 最短Hamilton路径 2. n≤100 → O(n^3) floyd,dp 3. n≤1000 → O(n^2),O(n^2log\_n) dp,二分,朴素版dijkstra,朴素版prim,Bellman-Ford 4. n≤10000 → O(n\*\sqrt{n}) 块状链表,分块,莫队 5. n≤100000 → O(nlog\_n) 各种排序sort,线段树,树状数组,set/map,heap,dijkstra+heap,prim+heap,spfa,凸包,半平面交,二分 6. n≤1000000 → O(n),常数较小的 O(nlog\_n) hash,双指针,并查集,kmp,AC自动机 常数较小的 O(nlog\_n):sort,树状数组,heap,dijkstra,spfa 7. n≤10000000 → O(n) 双指针,kmp,AC自动机,线性筛素数 8. n≤10^9 → O(\sqrt{n}) 判断质数 9. n≤10^{18} → O(log\_n) 最大公约数,快速幂 10. n≤10^{1000} → O((log\_n)^2) 高精度加减乘除 11. n≤ 10^{100000} → O(log*n \* loglog*n) 高精度加减,FFT/NTT 12. 其他 trie树、前缀和差分、区间合并、日期问题、单调栈单调队列、其他数学、输入输出、库函数 ## N<=30 ### dfs bfs 每位可选所有情况 枚举位置枚举选法dfs 指数型枚举 ```cpp int path[N]; void dfs(int u,int cut){ if(cut) return; if(u>n){ for(int i=1;i<=n;i++){ check dosomething } return; } for(int i=0;i a={...}; main: do{ int A=calc(...); int B=calc(...); int C=calc(...); if(check(A,B,C)){ dosomething } }while(permutation(a.begin(),a.end())); ``` n中选m 不同顺序算同一种 组合型枚举 ```cpp int way[N]; int a[N]={...}; int n,m; void dfs(int u,int start){ if(u+n-startm){ dosthing return; } for(int i=start;i<=n;i++){ way[u]=a[i]; dfs(u+1,i+1); way[u]=0; } } main: dfs(1,1); ``` 图 拓展 ```cpp 4方 int dx[4]={-1,0,1,0}; int dy[4]={0,1,0,-1}; 8向 int dx[8]={-1,-1,0,1,1,1,0,-1}; int dy[8]={0,1,1,1,0,-1,-1,-1}; 日 int dx[8]={-2,-1,1,2,2,1,-1,-2}; int dy[8]={1,2,2,1,-1,-2,-2,-1}; 日加田 int dx[12]={-2,-2,-1,1,2,2,2,2,1,-1,-2,-2}; int dy[12]={1,2,2,2,2,1,-1,-2,-2,-2,-2,-1}; ``` bfs回溯路径 ```cpp char p[choice]={'U','D','L','R'}; 有优先级 对应 void print_path(int x,int y){ if(x==startx && y==starty) if(x=0 && x<=n-1 && y>=0 && y<=m-1 && !st[x][y]; 0起 return x>=1 && x<=n && y>=1 && y<=m && d[x][y]==-1; 1起 return x>=0 && x<=n+1 && y>=0 && y<=m+1 && !st[x][y]; 染色 } ``` 读图 ```cpp 每行无空格 char g[N][M]; for(int i=0;i>g[i]; 每行有空格 int g[N][M]; for(int i=0;i>g[i][j]; } } 每行无空格的且长度不定 或字符需补圈0 int g[N][M]; for(int i=1;i<=n;i++){ string s; cin>>s; 数据中带空格用getline(cin,s); for(int j=1;j<=m;j++){ g[i][j]=s[j-1]-'0'; } } //题目说读n行 又给了空格 不知道是不是g[i][j]一个一个读入 万一只读3次就寄了 //按行处理 防恶心 cin>>n>>k; string s; getline(cin,s); for(int i=0;i PII; const int N,M; int n,m; char g[N][M]; int g[N][M]; int d[N][M]; int startx,starty,targetx,targety; ??char pre[N][M]; char p[choice]={...}; print_path() dx,dy,isVaild void bfs(int x,int y){ queue q; memset(d,-1,sizeof d); q.push({x,y}); d[x][y]=0; while(!q.empty()){ auto cur=q.front();q.pop(); int ux=cur.first,uy=cur.second; 对取出来的点的操作 if(ux==targetx && uy==targety){ dosomething print_path(ux,uy); return; return d[ux][uy];无需全找可以提前退出 } 拓展操作 for(int i=0;i>g[i][j]; } } for(int i=1;i<=n;i++){ string line; cin>>line; for(int j=1;j<=m;j++){ g[i][j]=line[j-1]-'0'; } } bfs(0,0); ``` 棋盘: ```text x,y (0,0) (0,1) (0,2) (0,3) (0,4) (1,0) (1,1) (1,2) (1,3) (1,4) (2,0) (2,1) (2,2) (2,3) (2,4) (3,0) (3,1) (3,2) (3,3) (3,4) (4,0) (4,1) (4,2) (4,3) (4,4) x+y (0) (1) (2) (3) (4) (1) (2) (3) (4) (5) (2) (3) (4) (5) (6) (3) (4) (5) (6) (7) (4) (5) (6) (7) (8) x-y+n (5) (4) (3) (2) (1) (6) (5) (4) (3) (2) (7) (6) (5) (4) (3) (8) (7) (6) (5) (4) (9) (8) (7) (6) (5) n-x+y (5) (6) (7) (8) (9) (4) (5) (6) (7) (8) (3) (4) (5) (6) (7) (2) (3) (4) (5) (6) (1) (2) (3) (4) (5) 正对角线 x+y 反对角线: n-x+y或者x-y+n 枚举每个位置或每一行 放与不放枚举 ``` ```cpp n皇后问题 //从点入手 #include using namespace std; #define endl '\n' const int N=10; char g[N][N]; bool row[N],col[N],dg[N*2],udg[N*2]; int n; void dfs(int x,int y,int cnt){ if(cnt>n) return; if(y==n) y=0,x++; if(x==n){ if(cnt==n){ for(int i=0;i>n; dfs(0,0,0); return 0; } //从行入手 #include using namespace std; #define endl '\n' const int N=10; char g[N][N]; bool col[N],dg[N*2],udg[N*2]; int n; void dfs(int u/*,int cnt*/){ // if(cnt>n) return;//与下面同理 n一定是刚好满足的 所以剪枝失效 cnt也可以不传入 if(u==n){ //if(cnt==n){ for(int i=0;i>n; for(int i=0;i using namespace std; const int N=210,INF=0x3f3f3f3f; int d[N][N]; int n,m,Q; void floyd(){ for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) d[i][j]=min(d[i][k]+d[k][j],d[i][j]); } int main() { cin>>n>>m>>Q; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(i==j) d[i][j]=0; else d[i][j]=INF; while(m--) { int a,b,w; cin>>a>>b>>w; d[a][b]=min(d[a][b],w); } floyd(); while(Q--) { int a,b; cin>>a>>b; int result=d[a][b]; if(result>INF/2) cout<<"impossible"<=v[i];j--){ f[j]=max(f[j],f[j-v[i]]+w[i]); } } cout<=k*v[i];j--){ f[j]=max(f[j],f[j-k*v[i]]+k*w[i]); } s[i]-=k; } if(s[i]){ for(int j=m;j>=s[i]*v[i];j--){ f[j]=max(f[j],f[j-s[i]*v[i]]+s[i]*w[i]); } } } ``` 混合背包 将01当做s[i]=1的多重背包 再将多重背包转成01背包求解 完全背包另做一种情况 ```cpp for(int i=1;i<=n;i++){ if(s[i]==0) for(int j=v[i];j<=m;j++) f[j]=max(f[j],f[j-v[i]]+w[i]); else{ if(s[i]==-1) s[i]=1; for(int k=1;k<=s[i];k*=2){ for(int j=m;j>=k*v[i];j--){ f[j]=max(f[j],f[j-k*v[i]]+k*w[i]); } s[i]-=k; } if(s[i]){ for(int j=m;j>=s[i]*v[i];j--){ f[j]=max(f[j],f[j-s[i]*v[i]]+s[i]*w[i]); } } } } cout<=v[i] && j2>=m[i]) f[i][j1][j2]=max(f[i-1][j1][j2],f[i-1][j1-v[i]][j2-m[i]]+w[i]); } } } cout<=v[i][k]) f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]); } } } cout<= v[i]) { if (f[i][j] < f[i-1][j-v[i]] + w[i]) { f[i][j] = f[i-1][j-v[i]] + w[i]; g[i][j] = g[i-1][j-v[i]]; } else if(f[i][j] == f[i-1][j-v[i]] + w[i]) { g[i][j] = (g[i][j] + g[i-1][j-v[i]]) % mod; } } } } int res = 0; for (int j = 0; j <= m; j++) { if (f[n][j] == f[n][m]){ res = (res + g[n][j]) % mod; } } cout << res; ``` 求具体方案(路径) 求完最大价值后 回过头来反着推一遍 看某个i是等于左边集合的状态还是右边集合的状态 若处于右边就说明这个i被选过 记录这些i ```cpp //1-n还是n-1看具体情况 1-n就是从小推大 上层i-1 反之亦然 for(int i=n;i>=1;i--){ for(int j=0;j<=m;j++){ f[i][j]=f[i+1][j]; if(j>=v[i]) f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]); } } for(int i=1,j=m;i<=n;i++){ if(j>= v[i] && f[i][j]==f[i+1][j-v[i]]+w[i]){ path[cnt++]=i; j-=v[i]; } } for(int i=0;i=v) f[i][j]=max(f[i][j],f[i-1][j-v]+w); } } ``` 体积恰好是j (求count/bool) 恰好是j 计数Count `f[0][0]=1` 没有选择任何物品且背包体积为0的方法只有1种 即不选任何物品 其他的`f[0][k] k > 0` 都初始化为0 在没有物品可选的情况下 不可能恰好填满非零体积的背包 ```cpp int f[N][N]; memset(f, 0, sizeof f); 可忽略 f[0][0] = 1; 不选择任何物品,且背包体积为0的情况 for(int i = 1; i <= n; i++) { for(int j = 0; j <= m; j++) { f[i][j] = f[i-1][j]; if(j >= v[i]) f[i][j]+=f[i-1][j-v[i]]; } } f[n][m]为恰好填满容量为m的背包的方法总数 ``` 恰好是j 布尔Bool `f[0][0]`初始化为true 不选择任何物品且背包体积为0是一种可能的情况 其他的`f[0][k] k > 0` 初始化为false 在没有物品可选的情况下 不能恰好填满非零体积的背包 ```cpp bool f[N][N]; memset(f, false, sizeof f); f[0][0] = true; for(int i = 1; i <= n; i++) { for(int j = 0; j <= m; j++) { f[i][j] = f[i-1][j]; if(j >= v[i]) f[i][j] |= f[i-1][j-v[i]]; } } f[n][m]最终会告诉我们是否存在至少一种方法来恰好填满容量为m的背包。 ``` 恰好为j max `f[0][0]`之外的所有值为负无穷大 表示初始时除了体积为0的情况外 其他所有体积的最大价值都是不可达的 ```cpp int f[N][M]; memset(f, -0x3f, sizeof(f)); f[0][0] = 0; for(int i = 1; i <= n; ++i) { for(int j = 0; j <= m; ++j) { f[i][j] = f[i-1][j]; if(j >= v[i]) { f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]); } } } int result = -1; for(int j = 0; j <= m; ++j) { if(f[n][j] > result) { result = f[n][j]; } } cout<=1;i--){ for(int j=1;j<=i;j++){ f[i][j]=max(f[i+1][j+1],f[i+1][j])+f[i][j]; 右下 左下 } } cout<右下 和 右下->左上 合成 左上->右下 上下左右四个方向 只会走 右 下 走两次 不能走同一点和一点只取一次等价 两条路径不会走同点两次 所以每点状态由左边和上边推出 求最大 ```cpp for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ f[i][j]=max(f[i-1][j]+w[i][j],f[i][j-1]+w[i][j]); } } cout<=1;i--){ f_dw[i]=1; for(int j=n;j>i;j--){ if(w[j]stk[top]){ stk[++top]=a[i]; } else{ *lower_bound(stk+1,stk+top+1,a[i])=a[i]; } } cout<>1; if(q[mid]>=w[i]) r=mid; else l=mid+1; } if(q[r]>1; if(check(m)) r=mid; else l=mid+1; } return r; SL(0,n-1,k) 等价于 lower_bound(nums,nums+n,k)-nums; 返回的是指针 减去首地址即为答案下标 ----| ---- 找最大(最右边) int l=0,r=N; while(l>1; if(check(m)) l=mid; else r=mid-1; } return r; 等价于 upper_bound(nums,nums+n,k)-nums-1; 升序数组中 upper_bound(a.begin(), a.end(), x); 查找第一个 > x的元素 lower_bound(a.begin(), a.end(), x); 查找第一个 >= x的元素 降序数组中: upper_bound(a.begin(), a.end(), x, greater()); 查找第一个 < x的元素 lower_bound(a.begin(), a.end(), x, greater()); 查找第一个 <= x的元素 ``` ### dp 见n<=100 ### dijkstra 正权边最短路 贪心思想 ```cpp #include using namespace std; const int N=510; int n,m; int g[N][N];//稠密图用邻接矩阵 int dist[N];//起点到各点的距离 bool st[N]; int dijkstra(){ //距离初始化成正无穷 memset(dist, 0x3f, sizeof dist); dist[1] = 0;//起点距离初始化为0 以它为中心 找其他点 for (int i = 0; i < n - 1; i ++ ){ int t = -1; for (int j = 1; j <= n; j ++ ) //找到一个离该点最近的点 if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; for(int j = 1; j <= n; j ++ ) //以新点为中心更新距离 dist[j] = min(dist[j], dist[t] + g[t][j]); st[t] = true; } if (dist[n] == 0x3f3f3f3f) return -1; return dist[n]; } int main() { scanf("%d%d", &n, &m); memset(g,0x3f,sizeof g); while(m--){//读入m条边 int a,b,c; scanf("%d%d%d", &a, &b, &c); //a和b间可能有多条边 保留最短的就够了 g[a][b]=min(g[a][b],c); } printf("%d\n", dijkstra()); return 0; } ``` ```cpp #include using namespace std; typedef pair PII; const int N=1e6+10; int n,m; int h[N],w[N],e[N],ne[N],idx;//稀疏图用邻接表 比之前多了一个W[]表示权 int dist[N]; bool st[N]; void add(int a,int b,int c){ e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++; } int dijkstra(){ memset(dist,0x3f,sizeof dist); dist[1]=0; //优先队列(小根堆)里面存距离和点 队头永远是最小的 所以可以直接取出来操作 priority_queue,greater> heap; heap.push({0,1});//把距离0 第一个点 放入小根堆 while(!heap.empty()){ //取队头 也就是最近的一个点 把它加入集合 auto t=heap.top(); heap.pop(); int ver=t.second,distance=t.first; if(st[ver]) continue; st[ver]=true; //根据这个点进行拓展 for(int i=h[ver];i!=-1;i=ne[i]){ int j=e[i]; if(dist[ver]+w[i] < dist[j]){ dist[j]=dist[ver]+w[i]; heap.push({dist[j],j}); } } } if(dist[n]==0x3f3f3f3f) return -1; return dist[n]; } int main() { cin>>n>>m; memset(h,-1,sizeof h); while(m--){ int a,b,c; cin>>a>>b>>c; add(a,b,c); } cout< using namespace std; const int N=510,INF=0x3f3f3f3f; int n,m; int g[N][N];//稠密 int dist[N]; bool st[N]; int prim(){ memset(dist,0x3f,sizeof dist); int res=0; //集合最初为空 在里面加点 所以0~n for(int i=0;idist[j])) t=j; if(i && dist[t]==INF) return INF; //先标记再更新(防止环) if(i) res+=dist[t]; st[t]=true; //更新其他点到加入该点后的集合的距离(其实就是以该点为中心更新距离) for(int j=1;j<=n;j++) dist[j]=min(dist[j],g[t][j]); } return res; } int main() { cin>>n>>m; memset(g,0x3f,sizeof g); while(m--){ int a,b,c; cin>>a>>b>>c; g[a][b]=g[b][a]=min(g[a][b],c); } int t=prim(); if(t==INF) puts("impossible"); else cout< using namespace std; const int N=510,M=10010;//点数500,边数10000 稠密图 int n,m,k; int dist[N],backup[N];//距离和备份数组 //虽然为稠密图 但是并不用矩阵去存储 //用结构体数组去存储边(起点终点权) //然后直接遍历这个结构体数组 struct Edge{ int a,b,w; }edges[M]; void bellman_ford(){ memset(dist,0x3f,sizeof dist); dist[1]=0; for(int i=0;ib的边,得到dist[b], 在更新b->c的边的时候,如果使用了已经更新的dist[b]去更新dist[c], 其实是遍历了两条边,但bellman_ford算法在每一次遍历的时候, 只允许更新一条边,所以需要用一个backup数据, 保证不会发生一次遍历中更新多条边 */ memcpy(backup,dist,sizeof dist); for(int j=0;j>n>>m>>k; for(int i=0;i>a>>b>>w; edges[i]={a,b,w}; } /* 是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断, 而并非是if(dist[n] == INF)判断, 原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响, dist[n]大于某个与INF相同数量级的数即可 */ bellman_ford(); if(dist[n]>0x3f3f3f3f/2) puts("impossible"); else printf("%d\n",dist[n]); return 0; } ``` ## N<=1e4 ### 块状链表 ### 分块 ### 莫队 ## N<=1e5 ### 贪心,sort,heap 贪心问题没什么迹可寻 一般就是排个序 区间模型里的左右端点排序 哈夫曼模型的堆排序 短作业优先 重权值优先的sort排序 中位数平均数的sort排序 一种奇怪的是 短视 把较长的选择缩小成小选择 逢升就卖 数据范围还是蛮大的 到了1e6 到了这个数据范围确实没什么可写了就想一下贪心能不能行 后面几题的话 想不出来写法其实就可以直接蒙贪心 反正不亏 主要熟悉排序的自定义 ```cpp 单关键字 sort(a.begin(),a.end(),greater); struct People{ string name; int score; // 默认<表升序 重载变降序 bool operator<(const Peoplet& rhs) const { return score > rhs.score; } }; vector a; sort(a.begin(),a.end()); struct { bool operator()(int a, int b) const{ return a < b; } }cmp; sort(a.begin(),a.end(),cmp); ``` ```cpp 多关键字 struct Product { string name; int sales; double rating; //先按销量降序 再按评分降序 bool operator<(const Product& rhs) const { if (sales != rhs.sales) return sales > rhs.sales; return rating > rhs.rating; } }; bool compareProduct(const Product& a, const Product& b) { if (a.sales != b.sales) return a.sales > b.sales; return a.rating > b.rating; } sort(products.begin(), products.end(), compareProduct); ``` ```cpp 优先队列 //升序队列 小顶堆 great小到大 priority_queue ,greater> minheap; //降序队列 大顶堆 Less大到小 默认 priority_queue,less> maxheap; priority_queue maxheap; struct node1{ int x,y; bool operator<(const node1& other)const{ return x maxheap; struct node2{ int x,y; bool operator<(const node2& other)const{ return x>other.x; } }; priority_queue minheap; struct Person { string name; int age; Person(string n, int a) : name(n), age(a) {} }; struct CompareAge_max { bool operator()(const Person& a, const Person& b) { return a.age < b.age; // 较大的年龄优先 } }; struct CompareAge_min { bool operator()(const Person& a, const Person& b) { return a.age > b.age; // 较大的年龄优先 } }; priority_queue, CompareAge_max> people_maxheap; priority_queue, CompareAge_min> people_minheap; ``` ```cpp //默认都是Less sort(vec.begin(),vec.end(),less());//内置类型从小到大升序 priority_queue ,less > pql;//top出数据从大到小降序 sort(vec.begin(),vec.end(),greater());//内置类型从大到小降序 priority_queue,greater>pqg;//top出数据从小到大升序 struct node{ int x,y; }point; bool operator<(const node &a,const node &b) { if(a.x==b.x) return a.y>b.y; else return a.x Q; ``` ### set,map set 特点:set是一个存储唯一键值的有序容器。它不允许存储重复的元素。 底层实现:通常基于红黑树,元素按照特定顺序排序。 用途:适用于需要有序不重复元素集合的场景。 multiset 特点:与set类似,但它允许存储重复的元素。 底层实现:同样基于红黑树,元素自动排序。 用途:适用于需要有序可重复集合的场景。 unordered\_set 特点:存储唯一键值的无序容器,不允许重复元素。 底层实现:基于哈希表,元素无序存储。 用途:适用于追求高效插入、删除和查找操作的场景,不关心元素顺序。 map 特点:存储键值对,键唯一,自动根据键排序。 底层实现:通常基于红黑树。 用途:当需要根据键快速查找值时使用,适合需要有序键值对的场景。 multimap 特点:与map类似,但允许键重复,即一个键可以映射多个值。 底层实现:基于红黑树,元素自动按键排序。 用途:适用于需要将单个键映射到多个值的有序键值对集合场景。 unordered\_map 特点:存储键值对,键唯一,但存储无序。 底层实现:基于哈希表。 用途:当不需要元素排序,且追求高效的插入、删除和查找操作时使用。 有序与无序: set、multiset、map、multimap是有序容器,根据元素或键的顺序自动排序; 而unordered\_set和unordered\_map是无序容器,存储元素时不考虑顺序。 唯一与非唯一: set和unordered\_set只允许存储唯一元素,map和unordered\_map的键是唯一的; 而multiset和multimap允许存储重复的元素或键。 底层数据结构: 有序容器通常基于红黑树实现,保证了元素的有序性和较高的操作效率; 无序容器基于哈希表实现,提供了快速的访问速度。 ### 二分 见n<=1000 ### spfa ```cpp 最短路 #include using namespace std; const int N=1e6+10; int n,m; int h[N],w[N],e[N],ne[N],idx; int dist[N]; bool st[N]; void add(int a,int b,int c){ e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++; } int spfa() { memset(dist,0x3f,sizeof dist); dist[1]=0; queue q; q.push(1); while(!q.empty()){ auto t=q.front(); q.pop(); st[t]=false; for(int i=h[t];i!=-1;i=ne[i]){ int j=e[i]; if(dist[t]+w[i] < dist[j]){ dist[j]=dist[t]+w[i]; if (!st[j]){ q.push(j); st[j] = true; } } } } return dist[n]; } int main() { cin>>n>>m; memset(h,-1,sizeof h); while(m--){ int a,b,c; cin>>a>>b>>c; add(a,b,c); } int ans=spfa(); if(ans==0x3f3f3f3f) cout<<"impossible"< using namespace std; const int N=2010,M=10010; int h[N],w[M],e[M],ne[M],idx; int dist[N],cnt[N]; bool st[N]; int n,m; void add(int a,int b,int c){ e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++; } bool spfa(){ queue q; for(int i=1;i<=n;i++){ st[i]=true; q.push(i); } while(q.size()){ int t=q.front(); q.pop(); st[t]=false; for(int i=h[t];i!=-1;i=ne[i]){ int j=e[i]; if(dist[j]>dist[t]+w[i]){ dist[j]=dist[t]+w[i]; cnt[j]=cnt[t]+1; if(cnt[j]>=n) return true; if(!st[j]){ q.push(j); st[j]=true; } } } } return false; } int main() { cin>>n>>m; memset(h,-1,sizeof h); while(m--){ int a,b,c; cin>>a>>b>>c; add(a,b,c); } if(spfa()) puts("Yes"); else puts("No"); return 0; } ``` ### dijkstra,prim-heap 见n<=1000 ### 线段树 ### 树状数组 ### 凸包 ### 半平面交 ## N<=1e6 ### 双指针 (1)前后指针 在一个区间里面 用i和j维护一个答案区间 当答案不在区间内的时候 再移动ij指针使得答案包含在内 ```cpp for(int i=0,j=0;i2){ s[a[j]]--; if(!s[a[j]]) cnt--; j++; } res=max(res,i-j+1); } ``` 在这里面有一个特殊的问题 判断环路——快慢指针 ```cpp ListNode* detectCycle(ListNode* head) { ListNode* slow = head; ListNode* fast = head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; if (slow == fast) { slow = head; while (slow != fast) { slow = slow->next; fast = fast->next; } return slow; } } return nullptr; } ``` (2) 对撞指针 利用单调性 用俩指针(一个最大一个最小)去找到某个值(和) 根据值的情况调动指针 可以用在一个区间也可以用在两个区间里面 本质一样 ```cpp for(int i=1,j=N-1;i=1 && a[i]+a[j]>x) j--; if(j>=1 && a[i]+a[j]==x){ cout<<"YES"< using namespace std; const int N=100010; int fa[N]; //返回x所在集合的编号(祖宗结点) int find(int x){ if(fa[x]!=x) fa[x]=find(fa[x]);//路径压缩优化 return fa[x]; } int main(){ int n,m; cin>>n>>m; for(int i=1;i<=n;i++){ fa[i]=i;//初始化 让父节点指向自己 } while(m--){ char op[2]; int a,b; cin>>op>>a>>b; if(op[0]=='M'){ fa[find(a)]=find(b);//让a的祖宗结点等于b的祖宗结点 把a插在b里面 } else{ if(find(a)==find(b)) cout<<"Yes"< using namespace std; typedef pair PII; const int N=300010;//插入:10万 l:10万 r:10万 最大可能30万 int a[N],s[N];//a存的all中对应下标映射到原数轴对应的值 s为a求前缀和的数组 //存储(所有与插入和查询有关的)坐标 vector alls; //要进行两种操作 添加、查询 这两种操作都有两个数据 某位置插几、左右区间 //所以可以把这些待操作数作为一对pair来存入vector vector add,query; int find(int x) { //find做的就是让a数组的下标与alls数组下标对应 但是alls存的是下标 而a是有意义的值 int l=0,r=alls.size()-1; while(l>1; if(alls[mid]>=x) r=mid; else l=mid+1; } //这里映射为1完全是为了后面做前缀和操作 如果没这需求就直接一一对应就完事了 return r+1; } int main() { int n,m; cin>>n>>m; for(int i=0;i>x>>c; add.push_back({x,c}); alls.push_back(x); } for(int i=0;i>l>>r; query.push_back({l,r}); alls.push_back(l); alls.push_back(r); } //此时 alls数组中已经有了所有要用的下标 对它进行去重排序 sort(alls.begin(),alls.end()); //unique把不重复的放前面 重复了的放后面 返回不重复区间的末尾 //那么直接把unique返回的迭代器作为起始 把alls.end()为结束 删除这部分 就实现了去重 alls.erase(unique(alls.begin(),alls.end()),alls.end()); //处理添加操作 for(auto item:add) { int x=find(item.first); a[x]+=item.second; //注意这里 错两次了 他可能在同一个位置插入几次 所以是+= } //把a[]做前缀和 得到s[] 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< 5.45)插入unordered_map hash中hash["ABC"]=5.45 查询:hash["ABC"]会返回5.45 判断key是否存在:hash.count("ABC") != 0 或 hash.find("ABC") != hash.end() 遍历 for (auto &item : hash){ cout << item.first << ' ' << item.second << endl; } 或 for (unordered_map::iterator it = hash.begin(); it != hash.end(); it ++ { cout << it->first << ' ' << it->second << endl; } ``` #### 字符串哈希 用P进制换10进制的方式 P取经验值 131 即131进制的数转变成10进制(把字符串当成一个131进制数) 模上\(2^{64}\)等价于一个8字节的东西溢出 直接用unsigned long long存 将本该是问题的溢出巧妙转变成对\(2^{64}\)取模 由此可以处理出所有字符串的一个哈希值 为了求某一段显然是前缀和问题 预处理出所有前缀字符串的哈希值的前缀和数组 求某一段s[r]-s[l] 发现错了几位 得把前缀(l)部分 左移到与r 的高位匹配位置上去(后几位补0) 左移一次就是*p 左移两位就是*\(p^2\) 以此类推 要让l与r对齐 实际上是要左移r-l+1位 要快速求到\(p^{r-l+1}\) 每一位是乘上一个p的n次方 可以把p预处理出来 ```cpp #include using namespace std; typedef unsigned long long ULL; const int N=100010,P=131; char str[N]; ULL h[N],p[N]; int n,m; ULL get(ULL h[],int l,int r){ return h[r]-h[l-1]*p[r-l+1]; } int main() { cin>>n>>m; cin>>str+1; p[0]=1; for(int i=1;i<=n;i++){ h[i]=h[i-1]*P+str[i]; p[i]=p[i-1]*P; } while(m--){ int l1,r1,l2,r2; cin>>l1>>r1>>l2>>r2; if(get(h,l1,r1)==get(h,l2,r2)) puts("Yes"); else puts("No"); } return 0; } ``` ### KMP ```cpp 下标从1开始 #include using namespace std; const int N = 100010, M = 1000010; int n, m; int ne[N]; char s[M], p[N]; int main() { cin >> n >> p + 1 >> m >> s + 1; for (int i = 2, j = 0; i <= n; i ++ ){ while (j && p[i] != p[j + 1]) j = ne[j]; if (p[i] == p[j + 1]) j ++ ; ne[i] = j; } for (int i = 1, j = 0; i <= m; i ++ ){ while (j && s[i] != p[j + 1]) j = ne[j]; if (s[i] == p[j + 1]) j ++ ; if (j == n){ //匹配成功的操作 //printf("%d ", i - n); //…… j=ne[j];//还原现场 因情况而定 可以省略 也可能是从头开始 j=0; } } return 0; } 下标从0开始 #include using namespace std; const int N = 1000010; int n, m; char s[N], p[N]; int ne[N]; int main() { cin >> m >> p >> n >> s; ne[0] = -1; for (int i = 1, j = -1; i < m; i ++ ){ while (j >= 0 && p[j + 1] != p[i]) j = ne[j]; if (p[j + 1] == p[i]) j ++ ; ne[i] = j; } for (int i = 0, j = -1; i < n; i ++ ){ while (j != -1 && s[i] != p[j + 1]) j = ne[j]; if (s[i] == p[j + 1]) j ++ ; if (j == m - 1) { //匹配成功的操作 //printf("%d ", i - n); //…… j = ne[j];//还原现场 因情况而定 可以省略 也可能是从头开始 j=0; } } return 0; } ``` ### AC自动机 ### 其他 常数较小的O(nlogn)也能做 如 sort 树状数组 heap dijkstra spfa 见1e5 ## N<=1e7 ### 筛法 ```cpp 埃氏筛 从2开始 把所有质数的倍数都筛掉 没被筛的就是质数 保存起来 void get_primes(){ for(int i=2;i<=n;i++){ if(!st[i]){ //可以用质数就把所有的合数都筛掉; primes[cnt++]=i; for(int j=i;j<=n;j+=i) st[j]=true; } } } //用set方便些 set primes; void get_primes(int n){ for(int i=2;i<=n;i++){ if(!isnot_prime[i]){ primes.insert(i); for(int j=i;j<=n;j+=i) isnot_prime[j]=true; } } } 线性筛 只需要用最小的质因数进行筛除 注意避免重复标记if(i%primes[j]==0) 10^7用线性筛 void get_primes(){ for(int i=2;i<=n;i++){ if(!st[i]) primes[cnt++]=i; for(int j=0;primes[j]<=n/i;j++){ st[primes[j]*i]=true; if(i%primes[j]==0) break; } } } 区间筛 一个指定的区间 [L, U] 内筛选出所有的素数 与传统的埃拉托斯特尼筛法或线性筛法不同 区间筛专注于一个特定的区间 而不是从最小的素数开始对一个连续的整数序列进行筛选 这使得区间筛在处理大数值区间的素数查询时更加高效 (数据范围很大时 传统的从2开始的筛法就不符事宜了) 1. 筛选小于等于 sqrt{U} 的素数: 使用传统的埃筛或线筛筛出所有小于等于sqrt{U}的素数 区间[L, U]内的任何合数都可以被不大于sqrt{U}的素数筛去 2. 区间内素数筛选: 用得到的素数来筛选区间 [L, U] 内的素数 对于每一个小于等于sqrt{U}的素数p 找到它在区间[L,U]内的最小倍数 如果 L 不是 p 的倍数,则是下一个最近的 p 的倍数 然后从这个最小倍数开始,步进为 p,去除区间内所有 p 的倍数 const int N = 1e6 + 10; // 适用于大多数情况 bool isPrime[N]; // 标记数组 vector primes; // 存储小于等于 sqrt(U) 的素数 // 获取小于等于 n 的所有素数 void getPrimes(int n) { memset(isPrime, true, sizeof(isPrime)); isPrime[0] = isPrime[1] = false; // 0 和 1 不是素数 for (int i = 2; i <= n; i++) { if (isPrime[i]) { primes.push_back(i); for (int j = 2 * i; j <= n; j += i) { isPrime[j] = false; } } } } // 区间筛,筛选 [L, U] 内的素数 void segmentSieve(long long L, long long U) { memset(isPrime, true, sizeof(isPrime)); if (L < 2) L = 2; for (int p : primes) { if ((long long)p * p > U) break; long long start = max((long long)p * p, (L + p - 1) / p * p); for (long long j = start; j <= U; j += p) { isPrime[j - L] = false; } } for (long long i = 0; i <= U - L; i++) { if (isPrime[i]) cout << (L + i) << endl; // 输出区间 [L, U] 内的素数 } } int main() { int L, U; cin >> L >> U; // 输入区间 getPrimes((int)sqrt(U) + 1); // 预筛小于等于 sqrt(U) 的素数 segmentSieve(L, U); // 筛选区间 [L, U] 内的素数 return 0; } ``` ### 双指针 KMP AC自动机 见1e6 ## N<=1e9 ### 约数 ```cpp 试除法判断约数 从1到n枚举 找可以被n整除的数 又因为是成对出现的 所以又能缩小到求一半 另一半可以通过n/i求出来 set自动排序加去重 set get_divisors(int n){ set res; for(int i=1;i<=n/i;i++){ if(n%i==0){ res.insert(i); res.insert(n/i); } } return res; } 约数个数 类似排列问题 先求出所有的质因数 再使用 质因数的权值(指数)相乘 来计算出总个数 把一个数N 写成:N = (p_1^{x1})(p_2^{x2})(p_3^{x3})…(p_k^{xk}),其中pi为质数 则N的约数个数为:(x1+1)(x2+1)(x3+1)…(xk+1) unordered_map weight; for(int i=2;i<=n/i;i++){ while(n%i==0){ weight[i]++; n/=i; } } if(n>1) weight[n]++; for(auto x:weight){ res=res*(x.second+1); } 约数之和 同上 求出所有质因数 然后对于 每个基数都从 0~指数 累加起来 再进行相乘 把一个数N 写成:N = (p_1^{x1})(p_2^{x2})(p_3^{x3})…(p_k^{xk}),其中pi为质数 这N个约数之和为 (p_1^0+p_1^1+……+p_1^{x1})*……*(P_k^0+p_k^1+……+p_k^{xk}) 至于怎么算某一个(P_k^0+p_k^1+……+p_k^{xk}) 可以用秦九韶算法 也可以用一个temp每轮增长*base 再或者还可以用快速幂 以及等比数列公式…… unordered_map weight; for(int i=2;i<=n/i;i++){ while(n%i==0){ weight[i]++; n/=i; } } if(n>1) weight[n]++; LL res=1; for(auto prime:weight){ int base=prime.first,index=prime.second; LL temp=1,sum=1; while(index--){ temp=temp*base%mod; sum=(sum+temp)%mod; } res=res*sum%mod; } cout< divisor_count(MAX_N, 0); // 计算每个数的约数数量 void calculate_divisors(int n) { for (int i = 1; i <= n; ++i) { for (int j = i; j <= n; j += i) { divisor_count[j]++; // i 是 j 的一个约数 } } } // 每个数的约数数量 for (int i = 0; i < n; ++i) { cout << "Number " << numbers[i] << " has " << divisor_count[numbers[i]] << " divisors." << endl; } ``` ### 质数 合数成对出现 只需判断前半段 使用i≤n/i 效率高于sqrt(n) 避免出现i*i≤n的越界危险 ```cpp bool is_prime(int n){ if(n < 2) return false; for(int i = 2;i <= n / i;i ++){ if(n % i == 0){ return false; } } return true; } ``` 分解质因数 质因数的底数和指数: 底数指质数的基数 比如在分解12=2*2*3中 2和3就是底数 而指数是指底数出现的次数 2出现了两次 所以指数是2 x 的质因子最多只包含一个大于 根号x 的质数 如果有两个 这两个因子的乘积就会大于 x 矛盾 所以借鉴之前的思路 只需要枚举sqrt(x) i 从 2 遍历到 根号x 用 x / i,如果余数为 0,则 i 是一个质因子 s 表示质因子 i 的指数,x /= i 为 0,则 s++, x = x / i 最后检查是否有大于 根号x 的质因子,如果有,输出(如果有 只会有一个) ```cpp void divide(int x){ for (int i = 2; i <= x / i; i ++ ){//i <= x / i:防止越界,速度快于 i < sqrt(x) if (x % i == 0){//i为底数 int s = 0;//s为指数 while (x % i == 0) x /= i, s ++ ; cout << i << ' ' << s << endl;//输出 } } if (x > 1) cout << x << ' ' << 1 << endl;//如果x还有剩余,单独处理 cout << endl; } //或者 for(int i=2;i<=n/i;i++){ while(n%i==0){ h[i]++,n/=i; } } if(n>1) h[n]++;//大于sqrt(n) 的质因子 要么没有 要么只有一个 ``` 阶乘分解 求8!中的质因数分解 先针对单独的一个质因子2来说 1 2 3 4 5 6 7 8 乘完之后 一直while去模上2 就可以得到2的数量 2作为底数 数量作为指数 可以抽象成是一种按列算的方式 这里的阶乘特化 可以以一种按行算的角度来看问题 1 2 3 4 5 6 7 8 我们求得在8!中2的个数 1 1 1 1 首先我们先计算出2的倍数的个数:8/2=4 1 1 其次我们计算出4的倍数的个数: 8/4=2(上面求出了第一层,现在求第二层) 1 最后我们解出第三层的2的个数: 8/8=1 我们把4+2+1=7,所以一共7个2出现了。 即:cnt(x)=[n/(x^1)]+[n/(x^2)]+[n/(x^3)]+...(直到x的次方大于n) ```cpp const int N=1e6+10; bool isnot_prime[2*N]; set get_primes(int n){ set primes; for(int i=2;i<=n;i++){ if(!isnot_prime[i]){ primes.insert(i); for(int j=i;j<=n;j+=i) isnot_prime[j]=true; } } return primes; } // 计算N!的质因数分解 void factorize_factorial(int n, const set& primes) { for (int p : primes) { if (p > n) break; int count = 0; for (long long k = p; k <= n; k *= p) { count += n / k; } cout << p << " " << count << endl; } } ``` ## N<=1e18 ### gcd、lcm 最大公因数 对(a, b)连续使用辗转相除 直到小括号内 右边数字b为0 左边的数a 就是两数最大公约数 ```cpp while(b){ int c=a%b; a=b; b=c; } cout<>=1;` ```cpp long long qmi(long long a,int k,int p){ //注意a要传入long long a是指数级增长的 容易爆int long long res=1%p; //防止p=1 res=1%1=0 而不是 1 while(k){ if(k&1) res=res*a%p; k>>=1; a=a*a%p; //不传long long 的话 这里用 a=(long long)a*a%p; } return res; } ``` ## N<=1e1000 是位数不超过10的几次方 还是范围不超过10的几次方 ### int\_128 int: 2147483648, 即 \(2^{31}≈ 2×10^{10}\) long long :9223372036854775807 , 即 \(2^{63}≈9×10^{19}\) \_int128的最大值是:85070591730234615865843651857942052864 , 约为 \(10^{38}\) 换算成位的话 大概就是能表示38位的10进制数字 在大部分时候是可以解决高精度问题的 除非说什么涉及到 \(10^{几百位}\) 这样的数 ```cpp #include using namespace std; typedef __int128 LL; LL read() { LL x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + (ch - '0'); ch = getchar(); } return x * f; } void write(LL x) { if (x < 0) { putchar('-'); x = -x; } if (x > 9) write(x / 10); putchar(x % 10 + '0'); } int main() { LL a = read(); LL b = read(); LL sum = a + b; LL del = a - b; LL mul = a * b; LL div = a / b; LL mod = a % b; cout<<"sum: "; write(sum); cout< using namespace std; vector add(vector &A,vector &B) {//本质就是模拟加法竖式运算 vector C; //举个例子 79+23 //先是个位9+3 得12 其中1是要进到下一位的 2才是本位的值 //对于十位 实际上是7+2+1 为A的十位加B的十位加前面进的位 //回到个位其实也是这样 只不过前面进的位为0罢了 (9+3+0) //再回到十位 7+2+1=10 这时已经加完了 1不能进入下一轮的加法 所以得拎出来 push_back(1) //so 每一位实际是 当前位加当前位加上一位的进位 可以把这个值使用t去存储 然后将t分解成两部分 作为当前位和进位 int t=0; for(int i=0;i A,B; cin>>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'); auto C=add(A,B); //记得取出来的时候也是要逆着取 for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]); return 0; } ``` 高减 当前位等于 a的当前位*10 - b的当前位 - 上一位借的位(没借为0 借了为1) 借的位为 a-b的结果 大于0 说明无需借 为0 小于0 说明需要借 为1 当然为避免负值 采用先判断大小 让计算始终是大的减小的 如果要求是小的减大的 则计算大的减小的 把结果加个负号 ```cpp #include using namespace std; bool cmp(vector &A,vector &B) { //先比较位数 如果位数不等 直接返回A.size>B.size的判断结果 ture or false if(A.size()!=B.size()) return A.size()>B.size(); //如果位数相等 就逐个比 找到那个不同的数 返回A[i]>B[i]的判断结果 for(int i=A.size()-1;i>=0;i--) { if(A[i]!=B[i]) { return A[i]>B[i]; } } return true; } vector sub(vector &A,vector &B) { vector C; int t=0; //A的size一定大于B的size了 所以以A.size作为循环条件 //1234 存在数组为 [0]:4 [1]:3 [2]:2 [3]:1 //运算就是从低位开始算 [0]-[0]再考虑[1]-[1] //所以这里for循环用0~A.size for(int i=0;i1 && C.back()==0) C.pop_back(); return C; } int main() { string a,b; vector A,B; cin>>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); cout<<"-"; for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]); } return 0; } ``` 高乘 每一位等于 (当前位 * x + 上一位进位)% 10 进位等于 (当前位 * x + 上一位进位)/ 10 ```cpp #include using namespace std; //123*9 //3*9=27 7为当前位 进2 t%10 t/10 //2*9+2=20 0为当前位 进2 //1*9+2 11 1为当前位 进1 //得1107 //跟加法类似 就是一个取模得当前位 取余得进位的循环 //与手动计算乘法不一样的是 被乘数不是拆开来算的 而是当成一个整体 //比如 123*12 也是3*12=36 当前位6 进3 这样的一个过程 //需要注意的还有一点 循环截止条件不仅有高精度数取完 //还有一个就是最后要进位的情况 它是push_back在最后 //可以合并在一起 放在循环中 i mul(vector& A,int b) { vector C; int t=0; for(int i=0;i1 && C.back()==0) C.pop_back(); return C; } int main() { string a; int b; cin>>a>>b; vector 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--) printf("%d",C[i]); return 0; } ``` 高除 与之前不同 这里从高位开始算 每位等于(前一位除以x的余数*10 +当前位)除以 x 留给下一位的就是这个 (前一位除以x的余数*10 +当前位)模上x ```cpp #include using namespace std; //除法的话正着存其实会更好算些 但是因为这种高精度的计算要出现的话往往是同时出现 //所以为了统一 对于除法也用这种逆序储存 //123/3 //1/3=0 余1 //1*10+2 /3 =12 余0 //0*3+3 /3 =1 余0 //结果为41 余0 //注意的是 与加减乘不同 除是从高位开始算的 //每轮的被除数都是前轮的余数*10+当前位的数 //结果就是/b //余数就是%b //捋清楚其实也是很简单的一个循环 //最后要注意的就是 因为刚开始是从高位开始算的 而结果是push_back进去的 所以最后要反转一下C //别忘了取出前导0的问题 vector div(vector &A,int b,int &r) { vector 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 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<>x1>>y1>>x2>>y2; cout<>l>>r>>c; insert(l,r,c); } for(int i=1;i<=n;i++){ //s[i]=s[i-1]+b[i] b[i]+=b[i-1]; } for(int i=1;i<=n;i++){ cout<>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++){ cout<get_days(y,m)){ d=1; m++; if(m>12){ m=1; y++; } } } bool check_date(int y, int m, int d) { if (m < 1 || m > 12) return false; if(d < 1 || d > getdays(y,m)) return false; return true; } while ((curYear < 给定年份) || (curYear == 给定年份 && curMonth < 给定月份) || (curYear == 给定年份 && curMonth == 给定月份 && curDay < 给定日子)) { // 执行 next_day 操作 } ``` ### 单调栈 #### 单调栈 找最近 **用于找每个数 左/右边 最近的 比它小/大 的数** **(1)左边 最近的 小于它 的数** 从1~n遍历 序列应该是单调递增(保证栈头为最优解) 所以如果出现向下趋势 就不断出栈 ```cpp while(tt && x <= stk[tt]) tt--; //具体操作 //…… //把栈头作为答案保存 stk[++tt]=x;//把该元素入栈 ``` **(2)左边 最近的 大于它 的数** 从1~n遍历 序列应该是单调递减 所以如果出现向上趋势 就不断出栈 ```cpp while(tt && x >= stk[tt]) tt--; //具体操作 //…… //把栈头作为答案保存 stk[++tt]=x;//把该元素入栈 ``` **(3)右边 最近的 小于它 的数** 从n~1遍历 序列应该是单调递增 所以如果出现向下趋势 就不断出栈 ```cpp for(int i=n;i>=1;i--){ while(tt && h[i] <= h[stk[tt]]) tt--; //具体操作 //…… //把栈头作为答案保存 stk[++tt]=i;//把该下标入栈 } ``` **(4)右边 最近的 大于它 的数** 从n~1遍历 序列应该是单调递减 所以如果出现向上趋势 就不断出栈 ```cpp for(int i=n;i>=1;i--){ while(tt && h[i] >= h[stk[tt]]) tt--; //具体操作 //…… //把栈头作为答案保存 stk[++tt]=i;//把该下标入栈 } ``` 想清楚要找什么 考虑遍历方向 和答案应该是较大的还是较小的 较小的话 栈里应该是单调递增 这样才能保证栈顶是最合适的答案 较大的话 反过来 栈里应该是单调递减 #### 单调栈+二分 找最远 **(1)左边 最远的 大于它 的数** 比如 对于3来说 如果左边已经有了5 在53中间再来一个4是完全没有必要的 因为3肯定找到的是那个5 因为更大 且更远 由此发现是一个不出现递减趋势的序列 但个53中间有个7的话 这个7虽然不会被3找到 但是它可能会被后面的一个6找到 综上 构造一个单调递增的序列 若这个新加的数能保持递增趋势 说明不存在比它更大的数 没必要去找了 直接加进去 供后面的数去找 如果不能保持递增趋势 就说明左边有一个比它更大的数 在这个已有的单调序列中二分找到 ```cpp #include using namespace std; const int N=100; int a[N]; vector stk; int ans[N]; int n; int main() { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; //需要注意的一点是 stk存的是下标 for(int i=1;i<=n;i++){ //a[i]>a[stk.back()] 还是 a[i]>=a[stk.back()] //加不加等号看题意 //找严格小于它的数的话 这里加等号 意味着找到它自己 //找前面小于等于它的数的话 这里不加等号 if(stk.empty() || a[i]>=a[stk.back()]){//发现该元素比栈顶元素还大(保持单调递增) stk.push_back(i);//直接入栈 ans[i]=i;//记录答案 可能求长度等等 } else{//发现该元素不是最大 那么原单调序列里肯定存在一些比他大的数 二分找到最远的那个 int l=1,r=stk.size()-1; while(l>1; if(a[stk[mid]]>a[i]) r=mid; else l=mid+1; } ans[i]=stk[r]; } } for(int i=1;i<=n;i++) cout< using namespace std; const int N=100; int a[N]; vector stk; int ans[N]; int n; int main() { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=n;i++){ if(stk.empty() || a[i]>1; if(a[stk[mid]] using namespace std; const int N=100; int a[N]; vector stk; int ans[N]; int n; int main() { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=n;i>=1;i--){//只需改循环 if(stk.empty() || a[i]>a[stk.back()]){ stk.push_back(i); ans[i]=i; } else{ int l=0,r=stk.size()-1; while(l>1; if(a[stk[mid]]>a[i]) r=mid; else l=mid+1; } ans[i]=stk[r]; } } for(int i=1;i<=n;i++) cout< using namespace std; const int N=100; int a[N]; vector stk; int ans[N]; int n; int main() { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=n;i>=1;i--){ if(stk.empty() || a[i]>1; if(a[stk[mid]]=k) hh++; //求最小 递增区间 若出现向下 出队 while(hh<=tt && a[i]<=a[q[tt]]) tt--; q[++tt]=i; //当前区间的最大值为队头 b[i] = a[q[hh]]; } } ``` 找窗口的最大值 ```cpp void get_max(int a[],int b[],int tot,int k) { int hh=0,tt=-1; for(int i=0;i=k) hh++; //求最大 递减区间 若出现向上 出队 while(hh<=tt && a[i]>=a[q[tt]]) tt--; q[++tt]=i; //当前区间的最大值为队头 b[i] = a[q[hh]]; } } ``` 另外注意分析 我们要的答案是在插入新元素之前还是在插入新元素之后 ```cpp void get_max/* min */(int a[],int b[],int tot,int k) { int hh=0,tt=-1; for(int i=0;i=k) //这里一定有等于 因为要腾空间放新元素 满了就得出去 hh++; //具体操作也可能放在这里 //这是把新元素放进去之前就找答案 //最大最小只要改这里 最大应该是递减 最小应该是递增 出现异常就出队 //另外注意代入情景看等于是否影响结果 一般是也剔除 除非是要什么最左边的 才考虑去= while(hh<=tt && a[i]>/* < */=a[q[tt]]) tt--; q[++tt]=i; //具体操作可能放在这里 //这是把新元素放进去后再找答案 } } ``` stl版 ```cpp class Solution { public: vector maxInWindows/* minInWindows */(vector& nums, int k) { vector res; deque q; for(int i=0;i=k) q.pop_front(); //具体操作 情况2 //…… while(!q.empty() && nums[i]>= /* <= */ nums[q.back()]) q.pop_back(); q.push_back(i); //具体操作 情况1 //…… if(i>=k-1) res.push_back(nums[q.front()]); } return res; } }; ``` ### 区间合并 多次要对一个大区间的某些段做标记 可以暴力对每一段做标记 最后枚举整个区间 看是否有没标记过的 很多时候也可以用差分 某段处理后就在该段++ 不过我们要的结果不在乎具体是多少 只是有值就1 没值就0 然后对于暴力对每段做标记和差分的话 都会存在一个问题 有很多地方做了重复操作 这个时候就可以使用区间合并进行优化 把能合并的区间都合并起来 再对他们去做操作 就可以省去一些重复功 对于合并操作 得分析一个问题 题目中诸如:1~~3 4~~5 这样的能不能合并 如果能 模版处就改成ed+1 PII; PII range[N]; int cnt; range[cnt++]={l,r}; sort(range,range+cnt); int st=0,ed=0; for(int i=0;i #include using namespace std; int main() { double a = 3.0, b = 4.0; double c = sqrt(pow(a, 2) + pow(b, 2)); // 根据勾股定理计算斜边长度 cout << "Hypotenuse: " << c << endl; return 0; } ``` 对数log2 和 log10 计算二进制表示所需的位数 log2 函数计算以2为底的对数,这在需要处理与二进制相关的问题时非常有用。 比如,计算一个正整数在二进制表示中需要多少位。这对于算法竞赛中的位操作题目,比如求解一个数的二进制中1的数量,或者需要用位掩码表示某些状态时,非常实用。 ```cpp #include #include using namespace std; int main() { int n; cout << "Enter a positive integer: "; cin >> n; int bitsNeeded = log2(n) + 1; // 加1因为log2(n)计算的是索引,而位数从1开始计数 cout << "Bits needed for binary representation of " << n << ": " << bitsNeeded << endl; return 0; } ``` 这个例子假设`n`是一个正整数。log2(n)计算了n在二进制表示下的最高位的位置(从0开始计数),因此需要加1来得到总位数。 计算十进制数中的位数 log10函数计算以10为底的对数,它可以用来快速确定一个数在十进制表示中的位数。 这在处理需要数位操作的算法问题时特别有用,比如数字反转、数位和计算等。 ```cpp #include #include using namespace std; int main() { int n; cout << "Enter a positive integer: "; cin >> n; int digits = log10(n) + 1; // 加1因为log10(n)计算的是最高位的索引 cout << "Digits in the decimal representation of " << n << ": " << digits << endl; return 0; } ``` 这个例子演示了如何快速计算出一个正整数在十进制表示中的位数。类似于`log2`的用法,这里`log10(n)`计算的是`n`的最高位的位置索引,在十进制中,这个位置索引加1就是该数的总位数。 其他 modf - 分解浮点数 应用场景:如果要将一个浮点数的小时表示转换为小时和分钟(例如,将3.75小时转换为3小时45分钟),可以使用`modf`。 ```cpp #include #include using namespace std; int main() { double hours; cin >> hours; double intpart, fracpart; fracpart = modf(hours, &intpart); int minutes = round(fracpart * 60); cout << intpart << " hours and " << minutes << " minutes" << endl; return 0; } ``` fmod - 浮点数除法的余数 应用场景:计算一个飞轮每分钟转动7.5圈,经过98分钟后,它总共转了多少圈,并计算其相对于整圈的余数(即最后停留的位置)。 ```cpp #include #include using namespace std; int main() { double rotationsPerMinute = 7.5; double timeInMinutes = 98; double totalRotations = rotationsPerMinute * timeInMinutes; double remainder = fmod(totalRotations, 1); // 计算相对于整圈的余数 cout << "Remainder: " << remainder << " of a rotation" << endl; return 0; } ``` ### 输入输出 优化模版 ```cpp #include using namespace std; #define endl '\n' int main() { ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); return 0; } ``` 输入 getline 用法:getline(cin, stringVar); 目的:读取一整行文本,直到遇到换行符`\n`。`getline`会丢弃换行符,但会读取并保留行中的其他所有字符,**包括空格**。 场景:当你需要读取包含空格的一行文本或者确保一次读取直到行末尾时,`getline`是最佳选择。 注意:在getline之前使用cin或scanf读取其他数据,并且期望紧接着用getline读取下一行时,需要注意消耗掉留在输入缓冲区中的换行符 ```cpp cin>>n; getline(cin,s); for(int i=0;i> variable; 目的:读取数据,使用空格、制表符或换行符作为分隔符 cin会自动忽略任何前导空白字符 场景:适用于读取分隔开的单个数据项,如整数、浮点数、字符串(不含空格) 优点:使用方便,支持连锁调用(如cin >> a >> b;) 缺点:不能读取含有空格的字符串。 scanf 用法:scanf("%format_specifier", &variable); 目的:根据指定的格式读取数据。scanf提供了格式化输入的能力,可以按照特定的格式读取数据。 场景:当输入数据遵循固定格式,且需要精确控制输入格式时使用。它对于读取复杂格式化的输入非常有效。 优点:读取速度快,可以精确地控制输入数据的格式。 缺点:使用不当容易出错,如忽略返回值可能会导致未定义行为。对于字符串的处理不如`cin`和`getline`直观。 常用格式控制 整数 - `%d`:读取一个十进制整数。 - `%ld`和`%lld`:分别用于读取`long`和`long long`类型的十进制整数。 - `%u`:读取一个无符号十进制整数。 - `%x`或`%X`:读取一个十六进制整数。 - `%o`:读取一个八进制整数。 浮点数 - `%f`:读取一个浮点数(`float`或`double`)。 - `%lf`:读取一个`double`类型的浮点数。虽然`%f`在`printf`中用于`double`,但在`scanf`中`%f`是用于`float`的,`%lf`用于`double`。 字符和字符串 - `%c`:读取一个字符。 - `%s`:读取一个字符串,直到遇到空白字符(空格、制表符或换行符)为止。读取的字符串自动以空字符`\0`结尾。 其他 - `%p`:读取一个指针。 - `%%`:读取一个`%`字符。 读取多个值 `scanf`可以在一个调用中读取多个值,格式说明符之间的空格将被忽略,输入中的空格、制表符和换行符可以在任何格式说明符之间进行匹配。 ```cpp int a, b; scanf("%d %d", &a, &b); char ch; scanf(" %c", &ch); // 注意前面的空格,用于跳过前面的空白字符 double d; scanf("%lf", &d); char str[100]; scanf("%s", str); // 不需要&,因为数组名本身就是地址 ``` 注意事项 - 当使用%s读取字符串时,确保目标数组足够大,以避免缓冲区溢出。 - 使用%c读取字符时,如果想要忽略前面的空白字符(包括空格、制表符和换行符),可以在%c之前加一个空格,如scanf(" %c", &ch); - 对于scanf来说,必须提供变量的地址作为参数(使用&运算符),除了字符串数组因为数组名已经是地址。 - 使用scanf时要特别注意返回值,它返回成功读取的项目数。这个返回值可以用来检测输入是否按照预期进行。 getchar 用法:int c = getchar(); 目的:从标准输入读取下一个字符,并返回它。如果遇到文件结束符(EOF),则返回EOF。 场景:适用于需要逐字符读取输入时,如处理输入流中的空格、换行符等特殊字符。 总结 在算法竞赛中,选择哪种输入方法取决于具体任务的需求: 如果你需要读取整行数据,特别是包含空格的字符串,使用getline。 - 对于分隔的数据项,特别是不包含空格的字符串或数字,cin是一个方便的选项。 - 当输入格式非常具体,或者在性能极其关键的情况下,scanf可能是最好的选择,尽管它需要更多的注意来避免错误。 - 读取单个字符,逐字符处理,使用`getchar`: 输出 cout 用法:cout << value << ...; 目的:输出数据到标准输出。cout是C++中的标准输出流对象。 场景:适用于大多数输出需求,特别是当需要输出字符串、数字或是其他复合数据结构时。`cout`由于是类型安全的,因此在处理类似于字符串和数字混合输出时非常方便。 优点: - 类型安全,自动类型推断。 - 支持连锁调用,易于使用。 - 可以与C++标准库中的其他流对象一起使用,如文件流。 缺点: - 相比于`printf`,在处理格式化输出时可能不那么灵活。 - 在某些情况下,可能比`printf`慢,尤其是没有优化I/O性能时。 使用ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);进行优化 printf 用法:printf("format specifier", value1, value2, ...); 目的:根据指定的格式输出数据。printf是C语言中的标准输出函数,但在C++中仍然可用。 场景:当需要精确控制输出格式,比如指定浮点数的精度,或者输出的宽度时。 优点: - 高度格式化的输出。 - 在某些情况下,性能优于`cout`。 缺点: - 不是类型安全的,错误的格式说明符可能导致运行时错误 常用格式控制 printf非常强大,支持多种格式控制符,可以精确地控制输出格式。 - `%d`:输出十进制整数。 - `%u`:输出无符号十进制整数。 - `%f`:输出浮点数,默认情况下保留小数点后六位。 - `%.2f`:输出浮点数,小数点后保留两位。 - `%s`:输出字符串。 - `%c`:输出单个字符。 - `%x`或`%X`:输出十六进制数,`x`产生小写字母,`X`产生大写字母。 - `%p`:输出指针地址。 - `%%`:输出`%`字符。 高级用法 - 指定宽度:%5d表示输出的整数至少占5个字符宽度,如果数字位数不够,前面补空格。 - 左对齐:%-5d表示输出的整数左对齐,至少占5个字符宽度。 - 指定浮点数精度:%.3f表示输出的浮点数保留三位小数。 - 动态宽度和精度:%*.*f允许动态指定宽度和精度,这两个值由额外的参数提供。 ```cpp printf("%5d\n", 123); // " 123" printf("%-5d\n", 123); // "123 " printf("%.3f\n", 3.1415926); // "3.142" ``` puts和putchar puts: - 用法:puts(const char* s); - 目的:输出字符串`s`到标准输出,并自动在末尾添加换行符。 - 优点:简单易用,自动添加换行符,适合快速输出一行字符串。 - 缺点:只能输出字符串,不能格式化输出其他类型的数据。 putchar: - 用法:putchar(int char); - 目的:输出单个字符到标准输出。 - 优点:非常简单,用于输出单个字符。 - 缺点:每次只能输出一个字符,不适用于复杂的输出需求。 总结 根据需要输出的数据类型和格式化要求来决定。 `cout`适合大部分场景且易于使用; `printf`在需要特定格式时更为强大; 而`puts`和`putchar`适用于简单的输出需求,特别是当性能要求较高时。