再读《挑战程序设计竞赛》——出类拔萃(1)(2)

二分

二分的显著特征是区间单调性
前一部分满足条件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继续后移。

反转

反转问题有三个特点:

  1. 可以使用二进制压缩
  2. 同一操作区间反转第二次是多余操作
  3. 操作和顺序无关

线性结构的反转问题
蓝桥杯 翻硬币
规定连着翻两个。
简单的贪心:只要从左到右顺次去翻硬币,见到不符合的翻就行了。

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();
}

得到对我们有用的行和列,组成新的地图。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值