7段第一课:枚举

A 按钮变色

题目&解法

题目:有一个 \(4\times4\) 的网格上面有 \(16\) 个按钮。按钮只有黑(\(b\) )白( \(w\) )两种颜色。按动任意一个按钮,那么该按钮本身以及其上下左右的按钮(如果有的话)都会改变颜色(由白变黑,由黑变白)。给出初始按钮的状态,输出最少要按多少次按钮才可以让所有按钮变为同一种颜色。

解法1:爆搜即可,每个按钮有按下与不按下两种可能。可以把每个按钮进行编号,第一行是 \(0\)\(3\),第二行是 $4 $ 到 \(7\),以此类推,优点是方便 dfs 的时候枚举下一个

解法2:因为已经将每个按钮给了一个 \(0\)\(15\) 的编号,所以可以枚举 \(0\)\(2^{16}-1\) 的所有二进制,如果当前位置为 \(0\) 代表不以它为中心进行翻转,如果为 \(1\) 代表以它为中心进行翻转,其他与解法 \(1\) 类似

Code1 搜索做法

#include <bits/stdc++.h>
using namespace std;

char s[5][5];
int ans = 20;

char f(char c) {return c == 'w' ? 'b' : 'w';}
bool check() {
    char op = s[0][0];
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            if (s[i][j] != op) return false;
        }
    }
    return true;
}
void change(int x, int y) {
    s[x][y] = f(s[x][y]);
    if (x - 1 >= 0) s[x - 1][y] = f(s[x - 1][y]);
    if (x + 1 < 4) s[x + 1][y] = f(s[x + 1][y]);
    if (y - 1 >= 0) s[x][y - 1] = f(s[x][y - 1]);
    if (y + 1 < 4) s[x][y + 1] = f(s[x][y + 1]);
}
//id是当前的按钮编号,cnt是当前按下的按钮次数
void dfs(int id, int cnt) {
    if (id == 16) {
        if (check()) ans = min(ans, cnt);
        return ;
    }
    int x = id / 4;
    int y = id - x * 4;
    dfs(id + 1, cnt);
    change(x, y);
    dfs(id + 1, cnt + 1);
    change(x, y);
}
int main() {
    for (int i = 0; i < 4; i++) scanf("%s", s[i]);
    dfs(0, 0);
    if (ans != 20) printf("%d\n", ans);
    else printf("Impossible\n");
    return 0;
}

Code2 状压枚举二进制做法

具体做法为先枚举二进制,然后在判断每一位二进制是否为 \(1\)。判断方法为如果该二进制与上 \(2^i\) 大于零,说明第 \(i\) 位为 \(1\) ,可以参考以下这个例子:

假设当前枚举到了 \(5\),二进制位 \(101\),判断从右往左数的第 \(0\) 位是否为一就是 5&(1<<0) 的值大于零,转换到二进制就是 \(101\&1\),因为与有同为一则为一,否则为零的特性,所以结果为 \(1\) 大于 \(0\) ,所以第 \(0\) 位的值位 \(1\)

如果要判断从右往左数第 \(1\) 位的数值就是 5&(1<<1) 的值大于 \(0\),转换到二进制就是 \((101)\&(010)\),显然所有位都是 \(0\),最终结果为 \(0\),所以第 \(1\) 位的值位 \(0\)

枚举二进制的时候推荐数组以 \(0\) 开始,方便枚举

具体可以参考代码

#include <bits/stdc++.h>
using namespace std;

char s[5][5];
int ans = 20;

char f(char c) {return c == 'w' ? 'b' : 'w';}
bool check() {
    char op = s[0][0];
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            if (s[i][j] != op) return false;
        }
    }
    return true;
}
void change(int x, int y) {
    s[x][y] = f(s[x][y]);
    if (x - 1 >= 0) s[x - 1][y] = f(s[x - 1][y]);
    if (x + 1 < 4) s[x + 1][y] = f(s[x + 1][y]);
    if (y - 1 >= 0) s[x][y - 1] = f(s[x][y - 1]);
    if (y + 1 < 4) s[x][y + 1] = f(s[x][y + 1]);
}
int main() {
    for (int i = 0; i < 4; i++) scanf("%s", s[i]);
    //枚举二进制状态mask
    for (int mask = 0; mask < (1 << 16); mask++) {
        int cnt = 0;//记录翻转次数
        for (int end = 0; end < 16; end++) {//枚举第end位
            if (mask & (1 << end)) {//判断mask的第end位是否为1                
                cnt++;
                int x = end / 4;
                int y = end - x * 4;
                change(x, y);
            }
        }
        if (check()) ans = min(ans, cnt);
        for (int end = 0; end < 16; end++) {//回溯
            if (mask & (1 << end)) {               
                int x = end / 4;
                int y = end - x * 4;
                change(x, y);
            }
        }
    }
    if (ans != 20) printf("%d\n", ans);
    else printf("Impossible\n");
    return 0;
}

B 最短连续子序列

题目&解法

题目:给定一个长度为 \(n\) 的整数序列以及整数 \(S\),求最短的连续子序列的长度使得这个连续子序列的和大于等于 \(S\)

解法:第一种解法为二分,借助前缀和具有单调性这一特点来进行二分,这里不重点讲述

第二种解法为双指针:枚举左端点 \(l\),在 \(l\) 固定的情况下,让右端点 \(r\) 不断向右滑动,一直滑动到满足区间和大于等于 \(S\) 为止,这样就得到了一个当左端点为 \(l\) 的区间和大于等于 \(S\) 的最短的区间,接下来再去下一个左端点

双指针的核心就在于 \(r\) 只会加不会减回去,这样就 \(r\) 最多只会加 \(n\) 次,\(l\) 因为枚举的原因也只会枚举 \(n\),这样使用双指针做这道题目的时间复杂度只有 \(O(n)\),甚至比二分还要快

接下来解释为什么 \(r\) 不会减回去,举个例子假设数组为 1 2 5 4 3\(S\)11,当 \(l\) 的值为 \(1\) 时,\(r\) 应当取 \(3\)(下标从 \(1\) 开始),即数字 \(5\) 的位置,区间和为 \(12\) 大于等于 \(1\)。接下来当 \(l\) 的值为 \(2\) 的时候会发现 \(r\) 没必要减回去,因为原本当 \(l\) 的值为 \(1\) 的时候区间 \([l,r]\) 就是最短的满足条件的区间,这样如果删掉左边的数(即区间左端点向右移动)那么这个区间和就一定会小于 \(S\),既然它小于 \(S\),那么我只要让 \(r\) 向右移动寻找大于等于 \(S\) 的即可,所以 \(r\) 永远不会减回去,最多只会枚举 \(n\)

注意双指针一般推荐下标从 \(1\) 开始,这样可以有效的避免一些数组越界的问题

Code

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
const int INF = (int) 1e9;
int a[N];
int main() {
    int n, s;
    scanf("%d%d", &n, &s);
    int ans = INF;
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    int r = 0, sum = 0;//r为右端点,sum为当前区间之和
    for (int l = 1; l <= n; l++) {
        while(r + 1 <= n && sum < s) {//滑动右端点
            sum += a[r + 1];
            r++;//注意这里r的初始值为0,所以上面判断是r+1<=n,这里是加上a[r+1]后才r++
        }
        if (sum >= s) ans = min(ans, r - l + 1);
        sum -= a[l];
    }
    if (ans != INF) printf("%d\n", ans);
    else printf("0\n");
    return 0;
}

C 异或和

题目&解法

题目:给定长度为 \(n\) 整数序列,求有多少区间的异或和等于相加之和。

解法:解本题需要用的以下有关抑或的性质:

  • a^b<=a+b 原因很简单,异或为相同为一不同为负,那么如果异或出来的二进制结果某一位为一,那么一定原来的两个数一定一个为一,一个为0,不可能某名奇妙多出来一个一的,所以 a^b<=a+b
  • 如果 a^b<a+b,那么 a^b^c<a+b+c,由上一个性质可以推导出,如果 a^b<a+b,那么后面无论再异或哪一个数都无法弥补这部分小掉的部分

根据这两个性质,就可以得出做法:先枚举左端点,然后双指针右端点找到当左端点固定时最大的右端点,最后计算答案即可

值得注意的是,这一题的双指针和上一题有所不同,上一题右端点停止滑动的条件是满足条件,这一题停止滑动的条件是不满足条件,写的时候会在细节上有所不同

Code

#include <bits/stdc++.h>
using namespace std;

const int N = 200010;
int a[N];
int main() {
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    long long r = 0, xorsum = 0, sum = 0, ans = 0;//不开long long见祖宗
    for (int l = 1; l <= n; l++) {//注意这里和上一题的不同点和共同点
        while(r + 1 <= n && (xorsum ^ a[r + 1]) == sum + a[r + 1]) {//不同点是这里判断了下一个是否合法,共同点是都维护区间[l,r]为合法区间
            sum += a[r + 1];
            xorsum ^= a[r + 1];
            r++;
        }
        ans += r - l + 1;
        sum -= a[l], xorsum ^= a[l];
    }
    printf("%lld\n", ans);
    return 0;
}

D 最大余数

题目&解法

题目:给定 \(n\) 个整数, 从中选出若干个数字(每个数字最多选一次),使得它们的和取余 \(m\) 最大,求最大的余数。

解法:使用折半搜索,把数组分为两半,分别枚举组合并记录相加并取模后的值,然后在第一半里面二分查找相加取模最大的数,即二分查找小于等于 \(m-1-x\) 的最大值(取模 \(m\) 的最大值为 \(m-1\)\(x\) 为第二半正在枚举的组合相加取模的值),如果没有找到就直接取第一半里面的最大值(注意这是因为二分的数组为有序且取模了 \(m\) 所有的数全都小于 \(m\),所以可以这么写),记录答案,注意特判不和另一半相加只是单独自身的情况

这里给到一个枚举组合的技巧:直接枚举二进制即可,因为枚举组合有交换顺序视为同一个组合的性质,可以转换为二进制的同一个数不同位为 \(1\),写起来相对于 dfs 要简单很多

Code

代码为了方便查找最大值、查找相加取模最大值和去重等使用了 set 代替上述解法中的记录第一半结果的数组,可以相互转化,不过 set 相较而言更简单

采用了 set.upper_bound() 这个函数进行查找,它的功能是查找严格大于某个数的最小的数(第一个数),找不到会返回 set.end(),然后取前面一个数就变成了小于等于某个数的最大的数(最后一个数)

#include <bits/stdc++.h>
using namespace std;

int a[40], n, m;
set<int> st;
void init() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    int len = n / 2;
    for (int mask = 0; mask < (1 << len); mask++) {
        long long sum = 0;
        for (int end = 0; end < len; end++) {
            if (mask & (1 << end)) sum += a[end];
        }
        sum %= m;
        st.insert(sum);
    }
}
void solve() {
    int l = n / 2;
    int len = n - n / 2;
    int ans = *(--st.end());
    for (int mask = 0; mask < (1 << len); mask++) {
        long long sum = 0;
        for (int end = 0; end < len; end++) {
            if (mask & (1 << end)) sum += a[l + end];
        }
        sum %= m;
        ans = max(ans, sum);
        auto it = st.upper_bound(m - 1 - sum);
        if (it == st.end() || it == st.begin()) ans = max(ans, (sum + *(--st.end())) % m);
        else ans = max(ans, (sum + *(--it)) % m);
    }
    printf("%d\n", ans);
}
int main() {
    init();//输入并预处理第一半数组取模m的值
    solve();//计算第二半数组取模m的值并计算答案
    return 0;
}
posted @ 2025-11-09 09:52  sxr1023  阅读(13)  评论(0)    收藏  举报