习题2-2

习题2-2

象棋

二分图

  • nXn的棋盘,棋盘为1的地方可以放车,问最多能放多少个车(满足每行每列最多只有1个车)?

  • 二分图的X集合和Y集合

  • 边只能从X集合连向Y集合,不能X、Y集合内部连边

  • 将题目要求转换成二分图,X集合是行,Y集合是列,最终每一行只能匹配一个列

匈牙利算法

  • 时间复杂度是O(nm),n是X集合、Y集合大小的最小值,m是边数

  • 如果用邻接矩阵的话实现二分图,时间复杂度是O(n3*m),邻接表比邻接矩阵更快

  • 利用二分图的思想,画出二分图

  • 每个点最多匹配一个点

  • 最优解可能不唯一

  • next是指向下一条边,to是下一条边所连接的节点位置

  • 曾广路:未匹配点出发,到终点是一个未匹配点,中间的点是交错的,有匹配点,也有未匹配点

  • 匈牙利算法:一直寻找曾广路,直到没有就停止

    • mc==0 ,未匹配点,直接cnt+1;
    • dfs(mc[y]),又从y点跳回x集合,寻找新的可能
  • struct E {
    	// next,下一条邻接边
    	// to,本条边所指向的终点
    	int next,to;
    }e[M];
    
    // ihead:邻接表的头
    // cnt:邻接表大小
    // mc:表示每个点所匹配到的另一个点(匹配到的Y集合中的点)
    // vis:Y集合元素是否被访问过
    // 比如x集合的1号点与y集合的7号点匹配,那么mc[1] = 7,mc[7] = 1;
    // 如果x集合的2号点没有可以匹配的点,那么mc[2] = 0;
    int cnt,ihead[N],mc[N];
    bool vis[N];
    
    // 邻接表连边,表示连一条x到y的有向边
    // 邻接表是什么?
    // 先开一个数组,命名为ihead,首元素表示邻接表的头
    // 当next等于0了,就代表没有边了
    // 什么是单向边?
    //
    // x:起点
    // y:终点
    // cnt表示邻接表大小
    // 插入的过程相当于链表的前向插入
    void add(int x, int y){
        // 边的下标是从1开始的,到n
        // 假设传入的第一条边是3到4
        // e[1].next = ihead[3]
        // e[1].to = 4;
        // ihead[3] = 1;
        // 第二条边是3到5
        // e[2].next = ihead[3];
        // e[2].to = 5;
        // ihead[3] = 2;
    	++cnt;
    	e[cnt].next = ihead[x];
    	e[cnt].to = y;
    	ihead[x] = cnt;
    }
    
    
    // 匈牙利算法
    // 一直寻找增广路,直到找不到
    //
    // x,y集合上的点,从当前点出发寻找增广路
    //
    // 什么是增广路?
    // x:1,2,3    y:4,5,6
    // 1和4已经匹配,2也连向4,此时去掉1和4的边,再从1号出发去寻找,发现1和5也是可以匹配的,这就找到了一条新的增广路
    // 从未匹配点出发,一直走走走,最后走到未匹配点,中间是匹配点和未匹配点交错,这样整条路径就是一条增广路
    //
    // 
    //
    // 假设最大匹配数是m,当前匹配数是k(k<m),一定存在一条增广路
    //
    // 返回值,若找到增广路则返回true,否则返回false
    // 永远都是从x集合进行dfs的
    bool dfs(int x){
    	for(int i=ihead[x];i!=0;i = e[i].next){
        	int y = e[i].to;
            if(!vis[y]){// 如果找到y集合上的点没有标记
            	vis[y] = true; // 标记该点
                // 如果y是没有匹配的点,说明找到一条增广路,或者说递归查找y的匹配点,得到了一条增广路
                // if mc[y] == 0  直接指向for循环里面的 不执行dfs(mc[y])
                // if mc[y] != 0  执行dfs(mc[y]) 已经标记的vis不会再走,如果有可以匹配的,则可以找到新的匹配点
                if(mc[y] == 0 || dfs(mc[y])){
                    // 找到增广路,我们要怎样更新mc数组
                    // 找到新的增广路,替换掉之前的增广路
     				mc[y] = x;
                    mc[x] = y;
                    return true;
                }
            }
        }
        return false
    }
    
    // 求解棋盘最多能放多少个车
    // n,棋盘大小
    // board是所给棋盘,1表示可以放车,0表示不能放车
    // 返回值,能放车的最大个数
    int getAnswer(int n,vector<vector<int>> board){
        // 我们将行看做n个点,将列看做另外n个点,标号分别是1到n和n+1到2n
        // 1到n是x集合的点,n+1到2n是y集合的点
    	cnt = 0;
        for(int i=1;i<=n;i++){
        	ihead[i] = 0;
            mc[i] = 0;
        }
        
        // 连边
        // 为什么是单向边?
        // i指向j+n
        // 为什么不需要j+n指向i,通过mc数组
        for(int i=1;i<=n;i++){
        	for(int j=1;j<=n;j++){
            	// 怎样连边,注意board的下标是从0开始的,别弄错
                if(board(i-1)(j-1)==1)
                    add(i,j+n);
            }
        }
        
        int ans = 0;
        for(int i=1;i<=n;i++){
            // 如果x集合中第i个点没有匹配到y集合上的点,则从这个点出发寻找增广路
        	if(mc[i]){
                // fill vis 全部为0
            	memset(vis,0,sizeof(bool)*(n*2+1);
                if(dfs(i))
                	++ans;
            }
        }
                       
        return ans;
    }
    
    
    

序列计数

  • 给定n个整数以及一个非负整数d,求有多少个长度大于1的连续子序列,满足该子序列的最大值最小值之差不大于d

解法1

  • 蛮力法,枚举左端点和右端点,记录所有的情况,计算他的差有没有超过d

  • long long ans = 0;
    for(int i=0;i<int(a.size());i++){
    	int mx = a[i];
    	int mn = a[i];
    	for(int j=i+1;j<int(a.size());i++){
    		mx = max(mx,a[j]);
    		mn = min(mn,a[j]);
    		if(mx-mn<=d)
    			ans++;
    	}
    }
    
  • 时间复杂度是O(n2)

解法1+

  • 若我们递增枚举左端点,可以发现右端点是单调不下降的

  • long long ans = 0;
    // 具有单调性
    // i:最远的为p,满足j in [i,...,p]都有a[i,..,j]最大值-最小值<=d
    // i+1 最远的为q,显然有p<=q
    // 用堆维护数列的最大值和最小值,这种访问方式的堆只能用bbst来实现
    // bbst tree
    tree.insert(a[0]);
    for(int i=0,j=i;i<int(a.size());i++){
        while(j+1 < int(a.size()) && 
              max(tree.getMin(),a[j+1]) - min(tree.getMax(),a[j+1])<=d ){
        	j++;
            tree.insert(a[j]);
     }
        tree.erase(a[i]);
        // j是最远的
        ans += j - i;
    }
    

解法2

  • 右端点是单调不下降的

    • 枚举左区间的端点i,找到pos从mid+1在右区间所能到达的最远距离
  • l==r,答案等于0

  • l<r时,切割成两半[l,mid],[mid+1,r],计算出两段内的连续子序列,再考虑cal函数

  • cal函数,枚举左半居间的端点,然后计算向右能拓展的最远的端点posi,则每个端点的序列个数是posi-mid

  • 怎么找posi?需要O(n)时间求出来

  • const int N = 300005;
    
    // n:题目中的n
    // d:题目中的d
    // max_value:用于存储solve函数中的前缀最大值
    // min_value:用于存储solve函数中的前缀最小值
    // a:题目中的a
    int n,d,max_value[N],min_value[N];
    vector<int> a;
    
    // 分治计算区间[l,r]中有多少个连续子序列满足最大值最小值之差不大于d
    // l:区间左边界
    // r:区间右边界
    // 返回值:满足条件的连续子序列的个数
    long long sovle(int l,int r){
        // 边界情况,我们不计算长度等于1的连续子序列,故返回0
    	if(l == r)
    		return 0;
    		
        // 中点
    	int mid = (l+r) >> 1;
        // 分治求出左右两半的值
    	long long ans = solve(l,mid)+solve(mid+1,r);
    	
        // 我们计算区间[mid+1,r]的前缀最小值和前缀最大值,也就是说min_value[i] = min(a[mid+1,...,i]),max_value同理
        // 计算的是右区间的前缀最大值、前缀最小值
    	for(int i=mid+1;i<=r;i++){
    		min_value[i] = (i==mid+1)?a[i]:min(min_value[i-1],a[i]);
    		max_value[i] = (i==mid+1)?a[i]:max(max_value[i-1],a[i]);
    	}
        
        // 计算的是左区间的后缀最大值、后缀最小值
        // 我们倒序枚举子序列的左端点i,i的取值范围在[l,mid]
        //
        // pos表示若连续子序列的左端点是i,那么子序列的右端点最远能拓展到pos位置,当然pos取值范围在[mid+1,r],一开始初始化为r
        // mn是后缀最小值,mx是后缀最大值,也就是说mn = min(a[i,...,mid]) mx同理
        // 那么以i为左端点的连续子序列(右端点在[mid+1,r]内)个数应该有pos-mid个
        int mn = 0,mx = 0,pos = r;
        for(int i=mid;i>=1&&pos>mid;i--){
            // 更新mn和mx
        	mn = (i==mid)?a[i]:min(mn,a[i]);
    		mx = (i==mid)?a[i]:max(mx,a[i]);
            // pos随着i的递减也会递减
            for(;pos>mid&&max(mx,max_value[pos])-min(mn,min_value[pos])>d;pos--);
            ans += pos-mid
        }
        return ans;
    }
    
    
    long long getAnswer(int n,int d,vector<int> a){
    	::n = n;
        ::d = d;
        ::a = a;
        return solve(0,n-1);
    }
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值