AtCoder - ABC 159 - E(二进制枚举,观察数据范围)

E.Dividing Chocolate

题目:

有 H×W 大小的方格矩阵,每个方格为 0 or 1, 现在可以进行横切与竖切,每次切都是一切到底的,询问最少切多少次可以保证最后的分块中每个分块都有不超过 K 个 1。

数据范围:

1 ≤ H ≤ 10
1 ≤ W ≤ 1000
1 ≤ K ≤ H × W

思路:

观察到题目给的H很小,用二进制暴力枚举横切的情况,二进制数范围为 0 ~ 2^{n-1} - 1。因为方块的宽度为h,说明可以横切的地方有n-1个位置,需要二进制位位数是n-1,所以二进制数最多枚举到2^{n-1} - 1。如:n=3,n-1=2,因为2^{2}=4=0100;而0~2^{2}-1:00,01,10,11。 

枚举横切时贪心求竖切的次数:

判断是否需要竖切,或者说在哪竖切时,对于横切后的各区域通过枚举列累加计数来判断是否某需要竖切,如果枚举时发现某区域到某列 j 时累计的 1 的个数超过 k ,说明需要在这一列 j 巧克力的左边界竖切一刀,也就是说是将这一列 j 划到下一块;

竖切过一刀后,以刀痕为界,再对右边的方块继续同上按区域枚举列,进而统计每区域 1 的个数,判断是否需要继续竖切,每竖切过一刀就有一个判断再竖切都没有用的情况:即单独判断需要开刀的拿一列(第 j 列) 1 的个数是否大于 k ,如果这一列拥有的 1 的个数已经大于 k ,那么之后再竖着切也没用了,必须横着切,直接跳出枚举竖切的循环,继续枚举横切即可。

反之,可以继续枚举竖切,同上继续枚举竖切直到竖切枚举完时才说明该方案成立;以此类推,用cnt 记录每次方案的刀数,取 Min 。

Code:

//#include<bits/stdc++.h>
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
using namespace std;

//#define int long long
#define x first
#define y second
#define IOS ios::sync_with_stdio(false);cin.tie(0);

//typedef long long LL;
typedef pair<int, int>PII;

const int N = 200010, INF = 0x3f3f3f3f;

int n, m, k, z;
char s[15][1010];
int c[15];

用来取一个二进制最低位的一与后边的0组成的数
//int lowbit(int x)
//{
//    return x & -x;
//}
//
累计二进制数x中1的个数
//int count1(int x)
//{
//    int res = 0;
//    while (x)
//        x -= lowbit(x), res++;
//    return res;
//}

//判断是否需要在方块的第j列的右边界竖着切一刀
bool infer(int j)
{
    int id = 0;                             //id代表横切分成的区域的编号
    for (int i = 0; i < n; i++)             //累计第j列拥有的1的个数
    {
        c[id] += (s[i][j] == '1');
        if (c[id] > k)return true;          //如果区域id块的1的个数大于k,需要竖切
        if (z >> i & 1)id++;                //根据枚举到的横切状态z通过右移&1,判断横切将方块分成几个区域(如:n=3,z=0011时,说明方块被完全横切两道,将方块上下分成了3个区域,依次对应idx=0,1,2)
    }
    return false;
}

void solve()
{
    cin >> n >> m >> k;
    for (int i = 0; i < n; i++)cin >> s[i];    //输入巧克力状态

    int ans = INF;

    for (z = 0; z < 1 << n - 1; z++)           //二进制枚举横切的状态0~2^(n-1)
    {
        memset(c, 0, sizeof(c));
        bool ok = false;

        int cnt = __builtin_popcount(z);       //记录横切了多少刀
        //int cnt = count1(z);

        for (int j = 0; j < m; j++)            //贪心判断竖切多少刀
        {
            if (infer(j))                      //判断此时横切后的状态是否需要竖切
            {
                cnt++;                         //竖切一刀
                memset(c, 0, sizeof(c));       //切完后,该刀痕右侧的部分竖着的每一块计数从0开始算

                ok = infer(j);                 //判断切后的划分到下一块的的这一列j拥有的1的个数是否大于k
                if (ok)break;                  //如果这一列已经大于k,说明横切少了,再竖切也无用,直接跳出循环继续枚举横切
            }
        }

        if (!ok)ans = min(ans, cnt);           //满足条件的方案记录下,取刀数最小值
    }

    cout << ans << endl;
}

signed main()
{
    IOS;

    int t = 1;
    //cin >> t;

    while (t--)
    {
        solve();
    }

    return 0;
}

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
吐槽:不得不说,这题我补的过程是真曲折啊,本来看的题解是不带break那条语句的,之后看懂后感觉应该加上应该没错,然后提交试试……一直过不了,一直以为我加的break不对,想半天,最后发现原来是因为我写循环时又将z当局部变量定义了一次,得,局部变量当全局变量用了,,,害-.-`。 本题注意点:

1.二进制枚举也接触过,不过感觉这题确定枚举范围那块还是有些不熟练。

2.还有这个统计二级制中1的个数函数 __builtin_popcount也是第一次见(⊙o⊙)…而且我的VS上编译显示找不到标识符(⊙o⊙)…但是能过。不过也可以用lowbit来实现该函数。

3.该题贪心判断是否竖切的过程也很巧妙,它在横切的基础上分区域记录,而且还在每竖切一次后单独判断该列是否可以说明竖切没用,少横切。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值