习题课2-3

习题课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轮仍然有点被更新即判定为无解?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值