二分
二分的显著特征是区间单调性
前一部分满足条件A,后一部分满足条件A的补集,求这两者的分界点。有二分性再考虑二分。
假定一个解判断是否可行 Cable Master
我们假定绳子长度为x,这是我们要二分的变量。
每二分出来一个结果,用C(x)函数来判断是否满足。
C(x)=L/x的结果>=K
如果满足C(x),就在x的右边取绳子的长度;如果不满足,就在x的左边取绳子的长度。
上题绳子的长度可以是小数,而在二分一个只取整数的a时,该如何处理呢?
在ACwing上看到的二分有两种模板:
1 找xxxxxxaoooooooo中的a
分为(L,mid)(mid+1,R)
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
2 找ooooooaxxxxxxxx中的a
分为(L,mid-1)(mid,R)
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
最大化最小值 Aggressive Cows
最大化最小值或者最小化最大值都是用二分来解决问题。
C(a)=牛的间距都大于等于a
a是我们的二分变量,即两头最近的牛的间距
写二分的时候其实最难的就是check函数,通常都是想出一个贪心策略得到,用于验证这个二分出来的x是否满足条件。
本题可以用贪心法求解
- 对牛舍的位置进行排序
- 第一头牛放入牛舍x[0],剩下的牛按满足距离大于a的条件下往后塞进牛舍。
应该用模板2(书上的写法也是对的,但是根据边界情况不同模板while(ub-lb>1)的判断条件也会相应变化,容易记混)
最大化平均值
C(x)=单位重量的价格大于等于x
求满足C(x)最大的x
将这个条件变形(很多题目数学变形就能发现新大陆)
check的贪心策略就是:排序后从大到小选取k个,看是否满足>=0,满足的话就搜x的右边,不满足就搜左边。
从书上例题出发讨论几个模拟的小技巧
3.2节介绍了尺取法、反转问题、折半枚举、离散化四个小技巧(弹性碰撞属于物理范畴在这不再赘述)。
尺取法
其实就是不断向后移动的双指针s和t。移动的方式和判断的方式因题目而异。
Subsequence
方法一:介于a[i]>0那么确定了起点的前缀和一定是单调递增的,可以枚举每个起点,二分寻找终点,再对每个起点的结果取min。
复杂度是O(nlogn)
方法二:
要想将其优化到O(n)就要起点和终点同时动起来。
初始化s=t=sum=0
用两个指针s和t表示起点和终点。如果sum<S,就继续t++
如果sum>=S,就更新ans为min(ans,sum)。然后将s++,继续判断。
看P148页的图就知道为什么叫尺取法了。
J‘s Reading Problem
这个与上一题不一样的是
1.判定条件,专门做一个数组看知识点有没有全覆盖就行了。
2.先让s=0,做出一个解,再不断让s++,把s覆盖的知识点cnt-1。那么当某个知识点覆盖次数为0时,又可以让t继续后移。
反转
反转问题有三个特点:
- 可以使用二进制压缩
- 同一操作区间反转第二次是多余操作
- 操作和顺序无关
线性结构的反转问题
蓝桥杯 翻硬币
规定连着翻两个。
简单的贪心:只要从左到右顺次去翻硬币,见到不符合的翻就行了。
Face the Right Way
假如i后面还有k个,我们的目的是找出i是否需要翻转。
取决于:
1.a[i]一开始是正面还是反面
2.i-k+1到i这一段位置翻转对i位置的影响,用sum计算i位置被额外翻转的次数,如果是偶数就相当于没翻转,是奇数就相当于翻转了一次。
如果遍历到i,而i是需要翻转的,就使f[i]=1表示从i到i+k-1被翻转了
统计在j到j+k-1的期间的每一次翻转
方法:对j到j+k-1中遍历的每个i,sum=sum+f[i]-f[i-k+1]
这样就不用在对i翻转的时候同时将后面k头牛转来转去,降低了时间复杂度
int solve(int k)
{
int f[10001];
int sum=0,ans=0;
for(int i=0;i+k<=n;i++)
{
if((a[i]+sum)%2!=0)
{
ans++;
f[i]=1;
}
sum+=f[i];
if(i-k+1>=0)
sum-=f[i-k+1];
}
for(int i=n-k+1;i<n;i++)
{
if((a[i]+sum)%2!=0)
{
return -1;
}
if(i-k+1>=0)
sum-=f[i-k+1];
}
return ans;
}
方格结构的反转问题 Fliptile
枚举第一行的反转方式即可。
#include<bits/stdc++.h>
using namespace std;
int mapp[600][600];
int a[600][600];
int d[4][2]={{0,-1},{1,0},{0,1},{-1,0}};
void press(int x,int y)
{
a[x][y]=!a[x][y];
for(int i=0;i<4;i++)
{
int xx=x+d[i][0];
int yy=y+d[i][1];
if(xx>=0&&xx<=4&&yy>=0&&yy<=4)
{
a[xx][yy]=!a[xx][yy];
}
}
}
int main()
{
int n;
cin>>n;
while(n--)
{
for(int i=0;i<5;i++)
{
string str;
cin>>str;
for(int j=0;j<5;j++)
{
mapp[i][j]=str[j]-'0';
}
}
int minstep=0x3f;
for(int i=0;i<1<<5;i++)
{
for(int k=0;k<5;k++)
for(int j=0;j<5;j++)
{
a[k][j]=mapp[k][j];
}
int p=i;
int cnt=0;
int pos=0;
while(p)
{
if(p&1==1)
{
press(0,pos);
cnt++;
}
p=p>>1;
pos++;
}
for(int k=1;k<5;k++)
{
for(int j=0;j<5;j++)
{
if(a[k-1][j]==0)
{
press(k,j);
cnt++;
}
}
}
int flag=1;
for(int k=0;k<5;k++)
{
if(a[4][k]==0)
{
flag=0;
break;
}
}
//cout<<endl;
if(flag)
{
minstep=min(minstep,cnt);
}
}
if(minstep>6)
cout<<-1;
else
cout<<minstep;
cout<<endl;
}
return 0;
}
反转延伸出来的二进制压缩问题
位运算无论是在状压dp还是二进制枚举中都有非常大的用处。
折半枚举
把要枚举的一半结果hash打表,另一半正常枚举得出结果后去表里面查,查到了就是一种方案。
有时候要枚举的是要打表的是两维变量,比如:
超大背包问题
离散化
这是个对一维数组的去重操作,也是离散化的核心:
a.erase(unique(a.begin(),a.end()),a.end())
区域的个数
在这道题里,用dfs求联通快就可以求出区域个数了。(书上用bfs是防止爆栈)
但是整个地图很大,有些行和上一行一样或者有些列和上一列一样可以忽略掉。
书上题解采用只存储有黑线的行(列)和他们的前一行(列)、后一行(列)。
for(int i=0;i<N;i++)
{
for(int d=-1;d<=1;d++)
{
//将周围的行加进去,列同理
int tx1=x1[i]+d;
int tx2=x2[i]+d;
if(1<=tx1&&tx1<=w)
xs.push_back(tx1);
if(1<=tx2&&tx2<=w)
xs.push_back(tx2);
}
}
即便如此仍然会有重复,用离散化去重,转化为新的坐标对。
//去重操作
sort(xs.begin(),xs.end());
xs.erase(unique(xs.begin(),xs.end()),xs.end());
//更新成新坐标
for(int i=0;i<N;i++)
{
x1[i]=find(xs.begin(),xs.end(),x1[i])-xs.begin();
x2[i]=find(xs.begin(),xs.end(),x2[i])-xs.begin();
}
得到对我们有用的行和列,组成新的地图。