习题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); }