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

浙公网安备 33010602011771号