前言
在算法竞赛和编程挑战中,博弈类问题往往要求我们充分理解参与者的行为模式和最优策略,从而提出合理的解法。在这篇博客中,我们将探讨三个有趣且富有挑战性的算法题:农夫约翰的奶酪块、蛋糕游戏和奶牛体检。这些问题涉及不同类型的博弈策略、优化问题以及动态规划技术。我们将逐一分析这些问题的背景、算法思路及解决方案,并提供详细的实现过程。
农夫约翰的奶酪块
农夫约翰有一块立方体形状的奶酪,它位于三维坐标空间中,从 (0,0,0)
延伸至 (N,N,N)。
农夫约翰将对他的奶酪块执行一系列 Q 次更新操作。
对于每次更新操作,农夫约翰将从整数坐标 (x,y,z) 到 (x+1,y+1,z+1) 处切割出一个 1×1×1 的奶酪块,其中 0≤x,y,z<N。
输入保证在农夫约翰切割的位置上存在一个 1×1×1 的奶酪块。
由于农夫约翰正在玩牛的世界,当下方的奶酪被切割后,重力不会导致上方的奶酪掉落。
在每次更新后,输出农夫约翰可以将一个 1×1×N 的砖块插入奶酪块中的方案数,使得砖块的任何部分都不与剩余的奶酪重叠。
砖块的每个顶点在全部三个坐标轴上均必须具有整数坐标,范围为 [0,N]。
农夫约翰可以随意旋转砖块。
输入格式
输入的第一行包含 N 和 Q。
以下 Q 行包含 x,y 和 z,为要切割的位置的坐标。
输出格式
在每次更新操作后,输出一个整数,为所求的方案数。
数据范围
2≤N≤1000,
1≤Q≤2×105,
0≤x,y,z<N
输入样例:
2 5
0 0 0
1 1 1
0 1 0
1 0 0
1 1 0
输出样例:
0
0
1
2
5
样例解释
在前三次更新操作后,[0,1]×[0,2]×[0,1] 范围的 1×2×1 砖块与剩余的奶酪不重叠,因此它贡献了答案。
算法思路
a[x][y] 记录的是在 x, y 平面上沿 z 轴的奶酪数量。
b[y][z] 记录的是在 y, z 平面上沿 x 轴的奶酪数量。
c[x][z] 记录的是在 x, z 平面上沿 y 轴的奶酪数量。
每次执行切割操作时,我们会更新这些数组的值,并检查是否能放置一个砖块。具体做法是:
- 每次切割后,我们更新这三个二维数组的值。
- 对于每一个更新操作,判断在 x, y, z 平面上的空位数量是否达到了砖块的大小 N,如果有空位,则可以插入砖块
根据当前的状态计算出能够插入砖块的数量。
- 如果 a[x][y] == n,表示在 x, y 平面上沿 z 轴有足够的空位来插入砖块。
- 如果 b[y][z] == n,表示在 y, z 平面上沿 x 轴有足够的空位来插入砖块。
- 如果 c[x][z] == n,表示在 x, z 平面上沿 y 轴有足够的空位来插入砖块。
代码如下
import java.io.*;
public class 农夫约翰的奶酪块 {
static PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StreamTokenizer st = new StreamTokenizer(br);
static int N = 1010; // 最大的 N 值
static int[][] a = new int[N][N], b = new int[N][N], c = new int[N][N]; // 记录平面上的奶酪数量
public static void main(String[] args) throws Exception {
int n = nextInt(); // 奶酪块的维度
int m = nextInt(); // 操作次数
int res = 0; // 砖块插入的方案数
// 处理每次的操作
while (m-- > 0) {
int x = nextInt(), y = nextInt(), z = nextInt(); // 切割的位置
// 更新平面 a, b, c
if (++a[x][y] == n) {
res++; // 如果在平面 a 上,沿 z 轴可以插入砖块,方案数加 1
}
if (++b[y][z] == n) {
res++; // 如果在平面 b 上,沿 x 轴可以插入砖块,方案数加 1
}
if (++c[x][z] == n) {
res++; // 如果在平面 c 上,沿 y 轴可以插入砖块,方案数加 1
}
// 输出当前的砖块插入方案数
pw.println(res);
}
// 刷新输出
pw.flush();
}
// 读取输入的整数
public static int nextInt() throws Exception {
st.nextToken();
return (int) st.nval;
}
}
蛋糕游戏
贝茜和埃尔茜发现了一行 N 个蛋糕(N 为偶数),大小依次为 a1,a2,…,aN。
两头奶牛都想吃到尽可能多的蛋糕。
但是,作为非常文明的奶牛,她们决定玩一个游戏来分割蛋糕!
游戏在两头奶牛之间轮流进行回合。
每个回合进行以下两者之一:
- 贝茜选择两个相邻的蛋糕并将它们堆叠起来,制造大小为两者大小之和的一个新蛋糕。
- 埃尔茜选择最左边或最右边的蛋糕藏起来。
当只剩下一个蛋糕时,贝茜吃掉它,而埃尔茜吃掉她藏起来的所有蛋糕。
如果两头奶牛都采取最优策略以最大化她们吃到的蛋糕量,并且贝茜先进行回合,那么每头奶牛将会吃到多少蛋糕?
输入格式
每个测试点包含 T 个独立的测试用例。
每个测试用例的格式如下。
第一行包含 N。
下一行包含 N 个空格分隔的整数 a1,a2,…,aN。
输出格式
对于每个测试用例,输出一行,包含 b 和 e,表示贝茜和埃尔茜在两头奶牛都采取最优策略的情况下分别吃到的蛋糕量。
数据范围
1≤T≤10,
2≤N≤5×105,
1≤ai≤109,
输入保证一个测试点中的所有 N 之和不超过 106。
输入样例:
2
4
40 30 20 10
4
10 20 30 40
输出样例:
60 40
60 40
样例解释
对于第一个测试用例,在最优策略下,
- 贝茜将堆叠中间两个蛋糕。现在蛋糕的大小为 [40,50,10]。
- 埃尔茜将吃掉最左边的蛋糕。现在剩余的蛋糕的大小为 [50,10]。
- 贝茜堆叠剩余的两个蛋糕。
贝茜将吃到 30+20+10=60 的蛋糕,而埃尔茜将吃到 40 的蛋糕。
第二个测试用例是第一个测试用例反转的情况,因此答案相同。
算法思路
贝茜的目标是尽可能多地合并蛋糕,使得最后剩下的蛋糕尽可能大。
埃尔茜的目标是尽可能多地藏起蛋糕,减少贝茜最后吃到的蛋糕量。
由于贝茜先手,她可以通过合并蛋糕来影响埃尔茜的选择。
使用前缀和数组 s 来快速计算任意区间的蛋糕总和。
对于每个测试用例,计算所有可能的区间和,找到最小的区间和 a,这个 a 就是贝茜最后吃到的蛋糕量。
埃尔茜吃到的蛋糕量就是总蛋糕量减去贝茜吃到的蛋糕量。
前缀和数组 s: 用于快速计算任意区间的蛋糕总和。s[i] 表示从第1个蛋糕到第i个蛋糕的总和。
区间长度 l: 由于贝茜和埃尔茜轮流操作,最终贝茜吃到的蛋糕量取决于某个区间的和。l 是区间长度的一半加1。
最小区间和 a: 通过遍历所有可能的区间,找到最小的区间和 a,这个 a 就是贝茜最后吃到的蛋糕量。
贝茜吃到的蛋糕量是 a,埃尔茜吃到的蛋糕量是总蛋糕量 s[n] 减去 a。
代码如下
import java.io.*;
public class 蛋糕游戏 {
static PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StreamTokenizer st = new StreamTokenizer(br);
static int N = 500010;
static int n;
static long[] s = new long[N];
public static void main(String[] args)throws Exception {
int t = nextInt(); // 读取测试用例数量
while (t-- > 0) { // 每个测试用例处理一次
n = nextInt(); // 读取蛋糕数量
int l = n / 2 + 1; // 计算贝茜操作时的长度限制
long a =(long) 1e15; // 初始化最小值为一个非常大的数
for (int i = 1; i <= n; i++) {
int x = nextInt(); // 读取每个蛋糕的大小
s[i] = x + s[i - 1]; // 更新前缀和
if(i >= l){ // 计算当前长度为 l 的区间和
a = Math.min(a, s[i] - s[i - l]); // 更新最小区间和
}
}
pw.println(a+" "+(s[n] - a)); // 输出贝茜和埃尔茜各自的蛋糕数量
}
pw.flush(); // 刷新输出流
}
// 快速读取整数
public static int nextInt()throws Exception{
st.nextToken(); // 读取下一个token
return (int) st.nval; // 返回整数值
}
}
奶牛体检
农夫约翰的 N 头奶牛站成一行,奶牛 1 在队伍的最前面,奶牛 N 在队伍的最后面。
农夫约翰的奶牛也有许多不同的品种。
他用从 1 到 N 的整数来表示每一品种。
队伍从前到后第 i 头奶牛的品种是 ai。
农夫约翰正在带他的奶牛们去当地的奶牛医院进行体检。
然而,奶牛兽医非常挑剔,仅愿意当队伍中第 i 头奶牛为品种 bi 时对其进行体检。
农夫约翰很懒惰,不想完全重新排列他的奶牛。
他将执行以下操作恰好一次。选择两个整数 l 和 r,使得 1≤l≤r≤N。反转队伍中第 l 头奶牛到第 r 头奶牛之间的奶牛的顺序。
农夫约翰想要衡量这种方法有多少效果。
对于每一个 c=0…N,请帮助农夫约翰求出使得恰好 c 头奶牛被检查的不同操作 (l,r) 的数量。
两种操作 (l1,r1) 和 (l2,r2) 不同,如果 l1≠l2 或者 r1≠r2。
输入格式
输入的第一行包含 N。
第二行包含 a1,a2,…,aN。
第三行包含 b1,b2,…,bN。
输出格式
输出 N+1 行,第 i 行包含使得 i−1 头奶牛被检查的不同操作 (l,r) 的数量。
数据范围
1≤N≤7500,
1≤ai,bi≤N
输入样例1:
3
1 3 2
3 2 1
输出样例1:
3
3
0
0
样例1解释
如果农夫约翰选择 (l=1,r=1),(l=2,r=2) 或 (l=3,r=3),则没有奶牛将会被检查。
注意这些操作并没有改变奶牛的位置。
以下操作会导致一头奶牛被检查。
- (l=1,r=2):农夫约翰反转第一头和第二头奶牛的顺序,因此新队伍中每头奶牛的品种将为 [3,1,2]。第一头奶牛将会被检查。
- (l=2,r=3):农夫约翰反转第二头和第三头奶牛的顺序,因此新队伍中每头奶牛的品种将为 [1,2,3]。第二头奶牛将会被检查。
- (l=1,r=3):农夫约翰反转第一头,第二头和第三头奶牛的顺序,因此新队伍中每头奶牛的品种将为 [2,3,1]。第三头奶牛将会被检查。
输入样例2:
3
1 2 3
1 2 3
输出样例2:
0
3
0
3
样例2解释
三种导致 3 头奶牛被检查的可能操作为 (l=1,r=1),(l=2,r=2) 和 (l=3,r=3)。
输入样例3:
7
1 3 2 2 1 3 2
3 2 2 1 2 3 1
输出样例3:
0
6
14
6
2
0
0
0
样例3解释
两种导致 4 头奶牛被检查的可能操作为 (l=4,r=5) 和 (l=5,r=7)。
算法思路
初始匹配:
- 在不进行任何操作时,统计初始状态下满足 ai == bi 的奶牛数量 cnt。
反转区间的影响:
反转区间 [l, r] 会影响区间内的奶牛匹配情况。
对于区间内的奶牛,反转后原本匹配的可能会不匹配,原本不匹配的可能会匹配。
枚举所有可能的区间:
遍历所有可能的区间 [l, r],计算反转后满足 ai == bi 的奶牛数量。
使用滑动窗口的思想,从每个位置 i 开始,向左右扩展区间。
初始化:
- 计算初始状态下满足 ai == bi 的奶牛数量 cnt。
枚举区间:
对于每个可能的中心点 i,枚举区间长度为 1 和 2 的情况(即奇数长度和偶数长度)。
对于每个区间 [l, r],计算反转后满足 ai == bi 的奶牛数量 sum:
减去区间内原本匹配的奶牛数量。
加上反转后新匹配的奶牛数量。
统计结果:
使用数组 ans 记录每个 c 对应的操作数量。
对于每个区间 [l, r],更新 ans[sum]。
输出结果:
- 遍历 ans 数组,输出每个 c 对应的操作数量。
代码如下
import java.io.*;
public class Main {
static PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StreamTokenizer st = new StreamTokenizer(br);
static int N = 7510;
static int n;
static int[] arr = new int[N], b = new int[N], ans = new int[N];
public static void main(String[] args) throws Exception {
n = nextInt();
for (int i = 1; i <= n; i++) {
arr[i] = nextInt(); // 读取奶牛品种序列
}
for (int i = 1; i <= n; i++) {
b[i] = nextInt(); // 读取兽医要求序列
}
// 计算初始匹配数量
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (arr[i] == b[i]) {
cnt++;
}
}
// 枚举所有可能的区间 [l, r]
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 2; j++) { // j=0 表示奇数长度,j=1 表示偶数长度
int sum = cnt; // 初始匹配数量
for (int l = i, r = i + j; l >= 1 && r <= n; l--, r++) {
// 减去原本匹配的
if (arr[l] == b[l]) {
sum--;
}
if (arr[r] == b[r]) {
sum--;
}
// 加上反转后新匹配的
if (arr[l] == b[r]) {
sum++;
}
if (arr[r] == b[l]) {
sum++;
}
ans[sum]++; // 更新结果
}
}
}
// 输出结果
for (int i = 0; i <= n; i++) {
pw.println(ans[i]);
}
pw.flush();
}
public static int nextInt() throws Exception {
st.nextToken();
return (int) st.nval;
}
}
总结
在编程竞赛中,理解博弈论和优化问题的解法是非常重要的。无论是通过动态规划、贪心算法,还是通过高效的数据结构来优化时间复杂度,这些技术都将帮助我们更好地解决实际问题。