这周周赛,又果断错过了,周一考驾照科三,周日就过去考场模拟,幸运的是,终于拿到驾照了,开shan。
第一题:暴力模拟。
第二题:简单的二元一次方程组求解,当然也可以暴力枚举,时间复杂度可以过。
第三题:暴力枚举(理论分析本来应该超时的,但没超时,应该是数据问题),或者动态规划DP。
第四题:动态规划DP。
详细题解如下。
1. 找出井字棋的获胜者(Find Winner on A Tic Tac Toe Game)
2. 不浪费原料的汉堡制作方案(Number of Burgers with No Waste of Ingredients)
3.统计全为 1 的正方形子矩阵(Count Square Submatrices with All Ones)
4.分割回文串 III(Palindrome Partitioning III)
LeetCode第165场周赛地址:
https://leetcode-cn.com/contest/weekly-contest-165/
1. 找出井字棋的获胜者(Find Winner on A Tic Tac Toe Game)
题目链接
https://leetcode-cn.com/problems/find-winner-on-a-tic-tac-toe-game/
题意
A 和 B 在一个 3 x 3 的网格上玩井字棋。
井字棋游戏的规则如下:
- 玩家轮流将棋子放在空方格 (" ") 上。
- 第一个玩家 A 总是用 "X" 作为棋子,而第二个玩家 B 总是用 "O" 作为棋子。
- "X" 和 "O" 只能放在空方格中,而不能放在已经被占用的方格上。
- 只要有 3 个相同的(非空)棋子排成一条直线(行、列、对角线)时,游戏结束。
- 如果所有方块都放满棋子(不为空),游戏也会结束。
- 游戏结束后,棋子无法再进行任何移动。
给你一个数组 moves,其中每个元素是大小为 2 的另一个数组(元素分别对应网格的行和列),它按照 A 和 B 的行动顺序(先 A 后 B)记录了两人各自的棋子位置。
如果游戏存在获胜者(A 或 B),就返回该游戏的获胜者;如果游戏以平局结束,则返回 "Draw";如果仍会有行动(游戏未结束),则返回 "Pending"。
你可以假设 moves 都 有效(遵循井字棋规则),网格最初是空的,A 将先行动。
示例 1:
输入:moves = [[0,0],[2,0],[1,1],[2,1],[2,2]] 输出:"A" 解释:"A" 获胜,他总是先走。 "X " "X " "X " "X " "X " " " -> " " -> " X " -> " X " -> " X " " " "O " "O " "OO " "OOX"
示例 2:
输入:moves = [[0,0],[1,1],[0,1],[0,2],[1,0],[2,0]] 输出:"B" 解释:"B" 获胜。 "X " "X " "XX " "XXO" "XXO" "XXO" " " -> " O " -> " O " -> " O " -> "XO " -> "XO " " " " " " " " " " " "O "
示例 3:
输入:moves = [[0,0],[1,1],[2,0],[1,0],[1,2],[2,1],[0,1],[0,2],[2,2]] 输出:"Draw" 输出:由于没有办法再行动,游戏以平局结束。 "XXO" "OOX" "XOX"
示例 4:
输入:moves = [[0,0],[1,1]] 输出:"Pending" 解释:游戏还没有结束。 "X " " O " " "
提示:
1 <= moves.length <= 9
moves[i].length == 2
0 <= moves[i][j] <= 2
moves 里没有重复的元素。
moves 遵循井字棋的规则。
解题思路
根据题目意思,其实就是小时候经常玩的一个“三子棋”,规则和五子棋差不多,只要谁先练成三个(横、竖、斜方向),谁就赢。
由题目要求,返回结果有以下几种可能:
1、谁赢就输出谁
2、如果没有赢,就判断还能不能下棋,如果不能下棋了,说明平局,就 "Draw"。如果还有的下,那就返回 "Pending"。
我们利用一个二维数组去模拟下棋情况,其中 0 表示没有下,1 表示 A 玩家下,2 表示B 玩家下。
先把moves的下棋情况更新到二维数组中,然后暴力枚举每一个点
- 如果是0,那就继续下一个点
- 如果是1 或者2,那就判断在这个位置的横、竖是不是都是1 或者2。如果这个位置刚好是对角线,同时判断对角线是不是都是1 或者2。如果是,那就是赢。否则没人赢,继续判断下一个点。
如果没有人赢,那就判断棋盘还有没有位置下(即判断moves数组长度是否等于9,因为棋盘最多有9个位置可以下棋)。如果没有位置了,那就说明平局"Draw"。还有位置可以下棋,那就"Pending"。
AC代码(C++)
class Solution {
public:
int pan[3][3];
bool isWin(int x, int y, int val)
{
// 判断同一行
if(pan[x][0] == pan[x][1] && pan[x][1] == pan[x][2] && pan[x][2]==val)
return true;
// 判断同一列
if(pan[0][y] == pan[1][y] && pan[1][y] == pan[2][y] && pan[2][y]==val)
return true;
// 判断两个对角线
if(x==y && pan[0][0]==pan[1][1] && pan[1][1]==pan[2][2] && pan[2][2]==val)
return true;
if((x+y)==2 && pan[0][2]==pan[1][1] && pan[1][1]==pan[2][0] && pan[2][0]==val)
return true;
return false;
}
string tictactoe(vector<vector<int>>& moves) {
// 初始化棋盘
memset(pan, 0, sizeof(pan));
for(int i = 0;i < moves.size();i+=2) pan[moves[i][0]][moves[i][1]] = 1;
for(int i = 1;i < moves.size();i+=2) pan[moves[i][0]][moves[i][1]] = 2;
// 枚举所有点
for(int i = 0;i < 3;i++)
{
for(int j = 0;j < 3;j++)
{
if(pan[i][j] == 0) continue;
if(pan[i][j] == 1)
{
if(isWin(i, j, 1)) return "A";
}
if(pan[i][j] == 2)
{
if(isWin(i, j, 2)) return "B";
}
}
}
if(moves.size() == 9) return "Draw";
return "Pending";
}
};
2. 不浪费原料的汉堡制作方案(Number of Burgers with No Waste of Ingredients)
题目链接
https://leetcode-cn.com/problems/number-of-burgers-with-no-waste-of-ingredients/
题意
圣诞活动预热开始啦,汉堡店推出了全新的汉堡套餐。为了避免浪费原料,请你帮他们制定合适的制作计划。
给你两个整数 tomatoSlices 和 cheeseSlices,分别表示番茄片和奶酪片的数目。不同汉堡的原料搭配如下:
- 巨无霸汉堡:4 片番茄和 1 片奶酪
- 小皇堡:2 片番茄和 1 片奶酪
请你以 [total_jumbo, total_small]([巨无霸汉堡总数,小皇堡总数])的格式返回恰当的制作方案,使得剩下的番茄片 tomatoSlices 和奶酪片 cheeseSlices 的数量都是 0。
如果无法使剩下的番茄片 tomatoSlices 和奶酪片 cheeseSlices 的数量为 0,就请返回 []。
示例 1:
输入:tomatoSlices = 16, cheeseSlices = 7 输出:[1,6] 解释:制作 1 个巨无霸汉堡和 6 个小皇堡需要 4*1 + 2*6 = 16 片番茄和 1 + 6 = 7 片奶酪。不会剩下原料。
示例 2:
输入:tomatoSlices = 17, cheeseSlices = 4 输出:[] 解释:只制作小皇堡和巨无霸汉堡无法用光全部原料。
示例 3:
输入:tomatoSlices = 4, cheeseSlices = 17 输出:[] 解释:制作 1 个巨无霸汉堡会剩下 16 片奶酪,制作 2 个小皇堡会剩下 15 片奶酪。
示例 4:
输入:tomatoSlices = 0, cheeseSlices = 0 输出:[0,0]
示例 5:
输入:tomatoSlices = 2, cheeseSlices = 1 输出:[0,1]
提示:
0 <= tomatoSlices <= 10^7
0 <= cheeseSlices <= 10^7
解题思路
送分题,都不知道这道题为啥是中等题。。。。。
发现两个汉堡都只需要一个奶酪,说明,如果答案存在的话,两个汉堡数之和为奶酪数cheeseSlices。根据数据大小,甚至可以枚举其中一个汉堡数量(从而得到另一个汉堡数量),利用需要的番茄数和等不等于tomatoSlices来判断是不是答案。
(暴力枚举没有提供AC答案,也不难,重点讲解下面求方程的方法)
数学好的,可以利用求解二元一次方程组来求解。我们假设两个汉堡数为 x 和 y。则可以得到方程
- 4x+2y = t (tomatoSlices)
- x + y = c (cheeseSlices)
通过解方程得到
- x = (t-2*c)/2
- y = (4*c-t)/2
为了答案x和y有效,那就是,不能为负数,不能为小数,所以判断 t-2*c 和 4*c-t,如果有复数,或者不是偶数,那就说明没有答案。
AC代码(C++)
class Solution {
public:
vector<int> numOfBurgers(int tomatoSlices, int cheeseSlices) {
int t = tomatoSlices, c = cheeseSlices;
int x = t-2*c, y = 4*c-t;
if(x<0 || y<0 || x%2==1 || y%2==1)
return {};
return {x/2, y/2};
}
};
3.统计全为 1 的正方形子矩阵(Count Square Submatrices with All Ones)
题目链接
https://leetcode-cn.com/problems/count-square-submatrices-with-all-ones/
题意
给你一个
m * n
的矩阵,矩阵中的元素不是0
就是1
,请你统计并返回其中完全由1
组成的 正方形 子矩阵的个数。示例 1:
输入:matrix = [ [0,1,1,1], [1,1,1,1], [0,1,1,1] ] 输出:15 解释: 边长为 1 的正方形有 10 个。 边长为 2 的正方形有 4 个。 边长为 3 的正方形有 1 个。 正方形的总数 = 10 + 4 + 1 = 15.
示例 2:
输入:matrix = [ [1,0,1], [1,1,0], [1,1,0] ] 输出:7 解释: 边长为 1 的正方形有 6 个。 边长为 2 的正方形有 1 个。 正方形的总数 = 6 + 1 = 7.
提示:
1 <= arr.length <= 300
1 <= arr[0].length <= 300
0 <= arr[i][j] <= 1
解题分析
根据题目的意思,首先想到的方法,依旧是暴力(暴力大法好呀)
方法一(暴力枚举):遍历每一个点,如果是1,那就枚举以该点为正方形左上角的所有正方形边长,判断这个这个边长的正方形内是不是全为1。
极限情况,如果刚好给的正方形是全1的话,那么复杂度会是O(N*M*min(N,M)*min(N,M)),这样子的话,分析复杂度应该会超时才对,但是最后提交时,居然没有超时。
所以应该是测试数据没有考虑极限情况。
方法二(动态规划DP):
如果我们假设,以 dp[i][j] 表示,以位置 (i , j)为正方形右下角点的最大正方形边长。那么这样子,知道这个最大边长dp[i][j]后,相当于这里面有以该点为正方形右下角点的正方形个数,那也就是答案 ans += dp[i][j]。
所以这就变成了一道动态规划问题,动态规划问题,最重要的三个部分
1、设好变量,也就是我们前面设的 dp[i][j]
2、找到状态转移方程,对于 dp[i][j] 的边长,首先得是,该点为 1。
要求这个点正方形的右下角点,就说明 dp[i-1][j-1],dp[i-1][j] 和dp[i][j-1],这三个中最小边长的再 + 1,就是dp[i][j] 的边长。
- 若形成正方形(非单
1
),以当前为右下角的视角看,则需要:当前格、上、左、左上都是1
- 可以换个角度:当前格、上、左、左上都不能受
0
的限制,才能成为正方形
为什么是要三者中最小值呢,因为以 (i,j)点,新的正方形,应当会包含 (i-1,j-1),(i-1,j),(i,j-1),也就是找到前三者的公共部分的正方形,再加上新的(i,j)成为一个新正方形。因此是找出三者最小值 + 1。
3、初始边界条件:那就是我们对所以点的正方形边长设为0,因为我们要找的是最大边长,所以设初始值为最小值。
这样子时间复杂度只需要为 O(N*M)
AC代码(方法一,暴力枚举 C++)
class Solution {
public:
bool isSquare(vector<vector<int>>& matrix, int x, int y, int len)
{
int m = matrix.size(), n = matrix[0].size();
// 新增加的边长,那就是右下角新的要为1,同时增加的右边一列,下边一行都要是1
if(matrix[x+len][y+len] == 0)
return false;
for(int i = x; i < x+len;i++)
{
if(matrix[i][y+len] == 0)
return false;
}
for(int i = y; i < y+len;i++)
{
if(matrix[x+len][i] == 0)
return false;
}
return true;
}
int countSquares(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
int ans = 0;
for(int i = 0;i < m;++i)
{
for(int j = 0;j < n;j++)
{
if(matrix[i][j] == 0) continue;
ans++;
for(int len = 1; len < min(m-i, n-j); len++) // 所有可能的边长
{
if(isSquare(matrix, i, j, len))
ans++;
else
break;
}
}
}
return ans;
}
};
AC代码(方法二,DP C++)
class Solution {
public:
int countSquares(vector<vector<int>>& matrix) {
int dp[305][305];
memset(dp, 0, sizeof(dp));
int n = matrix.size(), m = matrix[0].size();
int ans = 0;
for(int i = 1; i <= n;++i)
{
for(int j = 1;j <= m;++j)
{
if(matrix[i-1][j-1] == 1)
{
dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1;
ans += dp[i][j];
}
}
}
return ans;
}
};
4.分割回文串 III(Palindrome Partitioning III)
题目链接
https://leetcode-cn.com/problems/palindrome-partitioning-iii/
题意
给你一个由小写字母组成的字符串 s,和一个整数 k。
请你按下面的要求分割字符串:
- 首先,你可以将 s 中的部分字符修改为其他的小写英文字母。
- 接着,你需要把 s 分割成 k 个非空且不相交的子串,并且每个子串都是回文串。
请返回以这种方式分割字符串所需修改的最少字符数。
示例 1:
输入:s = "abc", k = 2 输出:1 解释:你可以把字符串分割成 "ab" 和 "c",并修改 "ab" 中的 1 个字符,将它变成回文串。
示例 2:
输入:s = "aabbc", k = 3 输出:0 解释:你可以把字符串分割成 "aa"、"bb" 和 "c",它们都是回文串。
示例 3:
输入:s = "leetcode", k = 8 输出:0
提示:
1 <= k <= s.length <= 100
s
中只含有小写英文字母。
解题分析
题目中,看到最少字符,考虑能不能用动态规划DP。重要的三步:
1、设变量,我们假设 dp[i][j] 表示,前 i 个字符,被分割为 j 段,所用的最少字符。
2、状态转移方程:
0~n切分成k个子串均为回文串的最小代价为
取最小值:
0~1切分成k-1个子串的最小代价 + 1~n为回文串的最小代价
0~2切分成k-1个子串的最小代价 + 2~n为回文串的最小代价
0~3切分成k-1个子串的最小代价 + 3~n为回文串的最小代价
....
0~n-2切分成k-1个子串的最小代价 + n-2~n为回文串的最小代价
0~n-1切分成k-1个子串的最小代价 + n-1~n为回文串的最小代价
因此,对于 dp[i][j],而言,枚举所有 x = 0, x < i,即 dp[x][j-1],变到 dp[i][j],也就是说,我们把从字符串 x+1 到 i 成为一段,因此我我们要得出,这段如果成为回文串的最小代价 cost[x+1][i]
因此状态转移方程是
dp[i][j] = min(dp[i][j], dp[x][j-1] + cost[x+1][i]);
因为是找最小,所以是取 min。
3、初始边界条件
因为找的是最小值,所以我们先对所有的dp设最大值(为了找出min)。
同时,当字符为0的时候,无论分割为几段,最小代价都是0
vector< vector<int>> dp(n+1, vector<int>(k+1, INF));
for(int j = 0;j <= k;j++)
dp[0][j] = 0;
最后就是关于代价 cost[i][j] 的计算,表示字符串第 i 个到 第 j 个(从1开始的),为回文串的代价,那就是判断这一部分,如果要变为回文串的代价。即,根据回文串规则,依次从第一个与最后一个判断相不相同,不相同就代价 + 1(因为要回文串,所以要变动其中一个字符)。然后判断第二个与倒数第二个,依次类推。
AC代码( C++)
class Solution {
public:
const int INF = 0x3f3f3f3f;
int palindromePartition(string s, int k) {
int n = s.size();
// 代价函数
vector< vector<int>> cost(n+1, vector<int>(n+1, 0));
for(int i = 0;i < n-1;++i)
{
for(int j = i+1;j < n;++j)
{
int res = 0;
int l = i, r = j; // 从第 i 个到 第 j 个的是回文串代价
while(l < r)
{
if(s[l] != s[r]) res++;
++l;
--r;
}
// 因为我们后面dp的,是字符从1开始算的,所以代价函数也是一样
cost[i+1][j+1] = res;
}
}
// dp[i][j] -> 表示前 i 个字符 ,分割成了 j 段的需要修改的最少字符数。
vector< vector<int>> dp(n+1, vector<int>(k+1, INF));
// 初始条件,前0个字符分成若干段,需要修改的字符数为0.
for(int j = 0;j <= k;j++)
dp[0][j] = 0;
for(int i = 1;i <= n;++i)
{
for(int j = 1;j <= k;++j)
{
for(int x = 0;x < i;++x)
{
// 从 x分为j-1段,到i为j段,表示,从x+1到i为新的一段,那么这段为回文串的代价为cost
// 所以对于 i,j找出最小的,那就是遍历之前的 所有 x,j-1到这里时,最小需要的字符数是
dp[i][j] = min(dp[i][j], dp[x][j-1] + cost[x+1][i]);
}
}
}
return dp[n][k];
}
};