习题课2-3
楼尔邦德(lower_bound)
- 二分查找的前提是序列有序
- 所在可以在搜索的过程中缩短搜索序列
二分查找 找到>=x的最小值
-
蛮力法,找到不小于x的最小元素,循环遍历整个序列,ans记录答案,时间复杂度O(nQ),Q是数据组数
-
更高明的暴力,先把序列排序,一旦找到答案,即可退出,因为后面的数都比它大
-
先排序,排外序找到中间元素mid,mid>=x,则在左区间继续寻找,如果mid<x,则答案在右区间,不断重复这个过程,直到区间长度为1,区间缩小速率是n/2,总共是n/2k,所以k是O(logn),所以总共的时间复杂度是O(nlogn)+O(Qlogn)
-
vector<int> getAnswer(int n,vector<int> a,int Q,vector<int> query){ vector<int> ans; ans.clear(); // 二分查找,需要保证a有序 sort(a.begin(),a.end()); for(int i=0;i<Q;i++){ int key = query[i]; // 进行二分查找 // 不能保证所有数都大于x,或者所有数都小于x int l = -1, r = n,mid; while(l + 1 < r){ mid = (l+r) >> 1; // key就是x if(a[mid]<key) l = mid; else r = mid; } // 更新答案下标 // 分界线在l和r之间,分界线右边时大于x,因为分界线在l和r之间,所以答案就是r int pos = r; if(pos>=n) ans.push_back(-1); else ans.push_back(a[pos]); } return ans; }
-
与普通二分查找相同的元素的代码对比,细节是魔鬼
-
int binarySearch(int[] nums, int target) { int left = 0; int right = nums.length - 1; // 注意 // 此处是 while(left <= right) { // 注意 int mid = (right + left) / 2; if(nums[mid] == target) return mid; else if (nums[mid] < target) left = mid + 1; // 注意 else if (nums[mid] > target) right = mid - 1; // 注意 } return -1; }
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wbQKeWN0-1605967274130)(C:\Users\liusiping\AppData\Roaming\Typora\typora-user-images\image-20201010073202783.png)]
-
需要找到分界线,左边是<x,右边是>=x
-
l一定是在分界线左边,r一定在分界线右边的,
-
最开始l在最左边,r在最右边,判断中间元素后,将两者之一移动到分界线
-
直到l+1=r,分界线一定在l和r的中间
-
二分更普适的用于分数规划、二分答案中的
方法二
- 时间复杂度可以做到线性,O(n)+O(Q)的
- 对数组排序,同时也对询问数组排序
- 所以寻找到一个答案x0后,不需要再整个遍历了,而是从上次寻找到答案的位置继续往后寻找即可
- 得到的启示:对重复寻找每个问题的子答案,除了找到普适的方法后,重复去寻找,能不能根据上一问求出的答案,通过记录一些数据,来帮助下次求解答案的过程
- 找到结果后就break,而且扫描区域从上次扫描结束的区域开始即可,不需要扫描开始的区域
- 时间复杂度是O(n)+O(Q)的,A数组有一个询问指针a,Q数组也有一个询问指针b,a、b两个指针都是往右移动的,在整个扫一遍的过程中,可以找出所有的答案
最小交换
- 通过最少次的两两交换使的整个数列变得有序
- 起泡排序中交换次数等于逆序对数
- 在这道题目中,最小交换次数(答案ans)恰好等于逆序对数
- 为什么起泡排序可以使逆序对减小1?
- 因为xy两个数是相连的,其次,左边的元素一定是大于右边的,其他的排序不能保证这两个条件
归并排序
-
vector<int> seq,seqTemp; long long cnt; void mergesort(int l,int r){ if(l==r){ return; } int mid = (l+r) >> 1; mergesort(l,mid); mergesort(mid+1,r); int p = 1, q = mid+1; for(int i=1;i<=r;i++){ // 1.右半区间用完了 // 2.左半区间没有用完,并且左边的元素小于右边的元素 if(q>r || p<=mid && seq[p]<=seq[q]) seqTemp[i] = seq[p++]; else { seqTemp[i] = seq[q++]; // 规定只在右边插入时才统计逆序对 // 和q产生逆序对的一定是比他大的, // 首先p所在位置的地方一定是大于它的 // 所以[p,mid]部分都能与q产生逆序对 cnt += mid - p + 1; } } for(int i=1;i<=r;i++){ seq[i] = seqTemp[i]; } } long long getAnswer(int n,vector<int> a){ seq = a; seqTemp.resize(n); cnt = 0; mergesort(0,n-1); return cnt; }
最大的逆序对数目
- 用long存储,因为逆序对数目可能很大
- 最坏的情况,完全倒序序列, (n-1)*n/2,
- 如果n等于200000,数据范围肯定超过了int
方法二
-
树状数组(还有线段树、单调栈这些特殊的数据结构),优化暴力
-
树状数组
- 单点的快速修改
- 快速前缀和查询
-
for i = 1 to n // 快速前缀和查询 // a[i]必须是可以控制的量级,不然必须采用离散化的手段去处理 for j = 1 to a[i]-1 ans += cnt[j]; cnt[a[i]]++;// 单点修改
-
// 树状数组 #include <bits/stdc++.h> using namespace std; int n; int a[1005],c[1005]; //对应原数组和树状数组 int lowbit(int x){ // 求出x的二进制中从最低位(个位)到高位连续零的长度、 // C[i] = A[i - 2k+1] + A[i - 2k+2] + ... + A[i] return x&(-x); } void updata(int i,int k){ //在i位置加上k while(i <= n){ c[i] += k; i += lowbit(i); } } int getsum(int i){ //求A[1 - i]的和,是求1到i之间的和 int res = 0; while(i > 0){ res += c[i]; i -= lowbit(i); } return res; } int main(){ int t; cin>>t; for(int tot = 1; tot <= t; tot++){ cout << "Case " << tot << ":" << endl; memset(a, 0, sizeof a); memset(c, 0, sizeof c); cin>>n; for(int i = 1; i <= n; i++){ cin>>a[i]; updata(i,a[i]); //输入初值的时候,也相当于更新了值 } string s; int x,y; while(cin>>s && s[0] != 'E'){ cin>>x>>y; if(s[0] == 'Q'){ //求和操作 int sum = getsum(y) - getsum(x-1); //x-y区间和也就等于1-y区间和减去1-(x-1)区间和 cout << sum << endl; } else if(s[0] == 'A'){ updata(x,y); } else if(s[0] == 'S'){ updata(x,-y); //减去操作,即为加上相反数 } } } return 0; }
-
#include<iostream> #include<bits/stdc++.h> using namespace std; const int maxn=500001; int c[maxn]; struct Node { int v,index; bool operator < (const Node &b) const { return v<b.v; //从小到大排序 } }node[maxn]; int n; void add(int i) { while(i<=n) { c[i]++; i+=i&(-i); } } long long sum(int i) { long long res=0; while(i>0) { res+=c[i]; i-=i&(-i); } return res; } int main() { cin>>n; int a; for(int i=1;i<=n;i++) { scanf("%d",&a); node[i].index=i; node[i].v=a; } sort(node+1,node+1+n); long long ans=0; for(int i=1;i<=n;i++) { add(node[i].index); //离散化结果—— 下标等效于数值 ans+=i-sum(node[i].index); //得到之前有多少个比你大的数(逆序对) } cout<<ans; return 0; }
最短路径
迪杰斯特拉算法
-
const int N = 100005; typedef pair<int,int> pii; // graph:存放图,graph[i]表示的是节点i的出边,其中first存储到达的节点,second存储边权 // pq:辅助Dijkstra算法的优先队列 // flag:记录每个节点是否进行过松弛,1表示进行过,0表示未进行过 // mind:存储起点s到每个节点的最短路径长度 vector<pii> graph[N]; priority queue<pii,vector<pii>,greater<pii>> pq; bool flag[N]; int mid[N]; // n:节点数目 // m:双向边数目 // U.V.W:分别存放各边的两端点和边权 // s,l:分别表示起点和终点 // 返回值:s到t的最短路径长度 int shortestPath(int n,int m,vecot<int> U,vector<int> V,vector<int> W,int s,int t){ // 初始化,清空pq,graph,mind,flag while(!pq.empty()) pq.pop(); for(int i=1;i<=n;i++){ graph[i].clear(); } memset(mind,127,sizeof(mind)); memset(flag,0,sizeof(flag)); // 建图,连接图中各边 for(int i=0;i<m;i++){ graph[U[i]].push_back(make_pair(V[i],W[i])); graph[V[i]].push_back(make_pair(U[i],W[i])); } // 设置起点的最短路是0,并将起点加入优先队列中 mind[s] = 0; pq.push(make_pair(mind[s],s)); // 执行Dijstra算法 // 当t节点还没有去松弛其他节点 // while(!flag[t])这么写可以通过此道题,但是有问题 // 单源非单汇,起点唯一,终点不唯一,求到其他所有点的最短路就不适用了 // 无解情况,也不能解决 // while(!pq.empty())松弛最多仍然只执行n次 while(!pq.empty()){ int u = pq.top().second; // 取出堆顶元素 pq.pop(); // 每个节点最多做一次松弛 // 用u做松弛,就是用u去更新其他点的最短路径 if(!flag[u]){ flag[u] = 1; // 枚举所有u出发的边 for(vector<pii>::iterator it = graph[u].begin();it!=graph[u].end();it++){ int v = it->first,w=it->second; // 判断答案的优劣 // 通过走到u的最短路加上u到v的边权是否大于记录的走到v的最短路 if(mind[v]<=mind[u]+w) continue; // 进行答案的更新 mind[v] = mind[u]+w; // 将v伴随其最新的最短路径长度加入优先队列 pq.push(make_pair(mind[v],v)); } } } return mind[t]; }
-
Dijkstra算法只能解决边权非负的问题
拓展
- Dijkstra解决的是边权非负问题
- 带有负权边的题可能是无解的
- 负权环,无解,圈环的总和是-1,不断的绕圈,最短路径一直减一
bellman-ford(解决带有负权边的问题)
-
// 初始化mind数组 mind[s] = 0; for round 1 to n+1 // 枚举所有边(u,v,len) mind[v] : min(mind[v],mind[u]+len) // 本轮中没有点被更新 if() break; // round等于n+1 if() sout("无解");
思考题
-
1.时间复杂度
-
2.为什么n+1轮仍然有点被更新即判定为无解?