1 题目
在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。
示例 1:

输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4
示例 2:

输入:matrix = [["0","1"],["1","0"]]
输出:1
示例 3:
输入:matrix = [["0"]]
输出:0
提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 300matrix[i][j]为'0'或'1'
2 代码实现
c++
cpp
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
if (matrix.empty() || matrix[0].empty()){
return 0 ;
}
int m = matrix.size(); //行
int n = matrix[0].size() ; //列
vector<vector<int>> dp(m , vector<int>(n , 0));
int maxSide = 0;
for (int j = 0 ; j < n ; j ++){
if (matrix[0][j] == '1'){
dp[0][j] = 1 ;
maxSide = 1 ;
}
}
for (int i = 1 ; i < m ; i++){
if (matrix[i][0] == '1'){
dp[i][0] = 1 ;
maxSide = 1 ;
}
}
for (int i = 1 ; i< m ; i++){
for (int j = 1 ; j < n ; j ++){
if (matrix[i][j] == '1'){
dp[i][j] = min (min(dp[i -1][j] , dp[i][j -1 ]) , dp[i -1][ j -1]) + 1 ;
maxSide = max(maxSide , dp[i][j]);
}
}
}
return maxSide * maxSide ;
}
};
js
javascript
/**
* @param {character[][]} matrix
* @return {number}
*/
var maximalSquare = function(matrix) {
if(matrix.length == 0 || matrix[0].length == 0 ){
return 0 ;
}
let m = matrix.length ;
let n = matrix[0].length ;
const dp = new Array(m);
for (let i = 0 ; i < m ; i++){
dp[i] = new Array(n).fill(0);
}
let maxSize = 0 ;
for (let i = 0 ; i < m ; i++){
if (matrix[i][0] === '1'){
dp[i][0] = 1 ;
maxSize = 1 ;
}
}
for (let j = 0 ; j < n ; j++){
if (matrix[0][j] === '1'){
dp[0][j] = 1;
maxSize = 1 ;
}
}
for (let i = 1 ; i < m ; i ++){
for (let j = 1 ; j < n ; j ++){
if (matrix[i][j] == '1'){
dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1;
maxSize = Math.max (maxSize ,dp[i][j]);
}
}
}
return maxSize * maxSize ;
};
思考
真是无语做了这么多动态规划,这个题目一出来我还是没看懂。
正方形啥意思,也就是dp数组如果是维护二维的,那么两个维度的长度要一样长,可是应该怎么走呢,不像之前的有两个选择,我只是知道动规的思想在于用小的推出大的,也就是i - 1 的情况可以推出 i 的情况。
这该怎么办?
题解
我完全懂你的困惑!刚看到这道题时,就算做过很多动态规划,也会懵:二维矩阵、找最大全 1 正方形,完全不知道 DP 状态怎么定义,更不知道怎么转移。
我带你一步步从 0 到 1 理清思路,再写标准 C++ 代码。
一、核心思考(突破点)
1. 先想:暴力解法为什么不行?
- 遍历每个点作为正方形左上角
- 尝试扩展边长,检查是否全为 1
- 时间复杂度 O(mn·min(m,n)²),数据范围 300×300,会超时→ 必须用动态规划优化
2. 动态规划状态定义(关键突破)
我们定义:dp[i][j]:以矩阵中坐标 (i,j) 为 右下角的最大全 1 正方形的边长
为什么选右下角 ?因为正方形是由上方、左方、左上方三个小正方形拼接而成的,右下角能统一这三个方向的状态。
3. 状态转移方程(核心逻辑)
- 如果
matrix[i][j] == '0':这个点不可能作为正方形右下角,所以dp[i][j] = 0 - 如果
matrix[i][j] == '1':能构成的最大正方形边长 = 上方、左方、左上方三个 dp 值的最小值 + 1
公式:dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
直观理解:三个方向的最小边长决定了当前能扩展的最大上限,+1 就是加上当前这个点。
4. 初始化
- 第一行、第一列:如果是 '1',
dp[i][j] = 1(只能构成边长为 1 的正方形) - 用一个变量
maxSide记录遍历过程中最大的边长
5. 结果
面积 = 最大边长 × 最大边长
二、C++ 完整代码实现
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
// 空矩阵直接返回0
if (matrix.empty() || matrix[0].empty()) return 0;
int m = matrix.size(); // 行数
int n = matrix[0].size(); // 列数
// dp数组和原矩阵大小一致
vector<vector<int>> dp(m, vector<int>(n, 0));
int maxSide = 0; // 记录最大正方形边长
// 1. 初始化第一行
for (int j = 0; j < n; j++) {
if (matrix[0][j] == '1') {
dp[0][j] = 1;
maxSide = 1;
}
}
// 2. 初始化第一列(跳过(0,0),避免重复)
for (int i = 1; i < m; i++) {
if (matrix[i][0] == '1') {
dp[i][0] = 1;
maxSide = 1;
}
}
// 3. 遍历剩余位置,状态转移
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == '1') {
// 核心转移方程
dp[i][j] = min(min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1;
// 更新最大边长
maxSide = max(maxSide, dp[i][j]);
}
// matrix[i][j] == '0' 时 dp[i][j] 保持0,无需处理
}
}
// 面积 = 边长²
return maxSide * maxSide;
}
};
// 测试代码
int main() {
vector<vector<char>> matrix = {
{'1','0','1','0','0'},
{'1','0','1','1','1'},
{'1','1','1','1','1'},
{'1','0','0','1','0'}
};
Solution sol;
cout << sol.maximalSquare(matrix) << endl; // 输出4
return 0;
}
三、代码细节解释
- 边界处理:先处理空矩阵,避免数组越界
- 初始化 :第一行/列只有
1能构成长度为 1 的正方形 - 双重循环 :从
(1,1)开始遍历,用状态方程计算每个位置的最大边长 - 最大值更新:遍历中实时记录最大边长,最后计算面积
四、复杂度分析
- 时间复杂度:O(mn),只需要遍历一次矩阵
- 空间复杂度:O(mn),使用了和矩阵等大的 dp 数组✨ 优化:可以用一维 dp 数组将空间降到 O(n),但二维写法更易理解
五、你的困惑总结
你之前卡壳的点:
- 不知道 DP 数组定义什么 → 以当前点为右下角的最大正方形边长
- 不知道怎么转移 → 取三个方向最小值 + 1
- 没想到正方形的拼接规律 → 右下角是最优状态定义点
这道题是二维动态规划的经典模板题,记住这个状态定义和转移方程,同类题直接套用即可!
总结
- 状态 :
dp[i][j]= 以(i,j)为右下角的最大全 1 正方形边长 - 转移 :
matrix[i][j]='1'时,dp[i][j] = min(上,左,左上)+1 - 结果:最大边长的平方就是答案
先记住一句话
dp [i][j] 表示:以 (i,j) 这个格子为右下角,能画出的最大正方形的边长。
一、先看前提
cpp
if (matrix[i][j] == '1')
意思很简单:只有当前格子是 1,才有可能构成正方形; 如果是 0,直接跳过,dp 值就是 0。
二、核心代码到底在干嘛?
cpp
dp[i][j] = min( 上, 左, 左上 ) + 1;
我给你翻译成人话:
这三个位置分别代表什么?
- dp [i-1][j] 正上方 → 以正上方为右下角的最大正方形边长
- dp [i][j-1] 正左方 → 以正左方为右下角的最大正方形边长
- dp [i-1][j-1] 左上角 → 以左上角为右下角的最大正方形边长
为什么要取 min(最小值)?
因为正方形必须四边相等 。你能拼成多大,取决于最短的那一块。
为什么 +1?
因为当前这个 1 自己就是新的一层边长。
三、我给你举个超级直观的例子
情况 1
上方 = 1
左方 = 1
左上 = 1
最小值 = 1→ 当前 = 1 + 1 = 2
意思:上面、左边、左上角都能构成边长为 1 的正方形, 那我现在这个位置就能构成边长为 2 的正方形!
情况 2
上方 = 2
左方 = 2
左上 = 2
最小值 = 2→ 当前 = 2 + 1 = 3
意思:三个方向都能构成边长 2,那我现在能构成边长 3!
情况 3
上方 = 2
左方 = 1
左上 = 2
最小值 = 1→ 当前 = 1 + 1 = 2
因为左边最短,只能到 1,所以你最多只能扩展到 2。
四、用一张图彻底看懂
假设现在看的是中间这个格子 (i,j):
左上(i-1,j-1) 上方(i-1,j)
↑
左方(i,j-1) → 当前(i,j)
要形成正方形,必须三个方向都能补齐 。短板决定长度。
五、再简化到极致
你只需要记住这句口诀:
当前是 1,能拼多大看 上、左、左上 三个里面最小的那个再加 1 就是答案
六、那一句 maxSide 又是干嘛?
cpp
maxSide = max(maxSide, dp[i][j]);
很简单:遍历过程中,把所有位置能构成的最大边长记录下来。
最后面积 = 最大边长 × 最大边长。
最终总结(你一定能记住)
- 当前格子是 1 才能继续算
- 看上、左、左上三个值
- 取最小的那个
- +1 就是当前能构成的正方形边长
- 全程记录最大边长
自己写js简直是乱写
javascript
/**
* @param {character[][]} matrix
* @return {number}
*/
var maximalSquare = function(matrix) {
if(matrix.length == 0 || matrix[0].length == 0 ){
return 0 ;
}
let m = matrix.length ;
let n = matrix[0].length ;
const dp = new Array(m);
for (let i = 0 ; i < n ; i ++){
dp[i] = matrix[i];
}
let maxSize = 0 ;
for (let i = 0 ; i < m ; i++){
if (matrix[i][0] === '1'){
dp[i][0] = 1 ;
maxSize = 1 ;
}
}
for (let j = 0 ; j < n ; j++){
if (matrix[0][j] === '1'){
dp[0][j] = 1;
maxSize = 1 ;
}
}
for (let i = 1 ; i < m ; i ++){
for (let j = 1 ; j < n ; j ++){
if (matrix[i][j] == '1'){
dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1;
maxSize = Math.max (maxSize ,dp[i][j]);
}
}
}
return maxSize * maxSize ;
};
你的代码 3 个错误
错误 1:dp 数组初始化完全写错了!
你写的:
javascript
const dp = new Array(m);
for (let i = 0 ; i < n ; i ++){ // ❌ 循环条件错了,应该是 m
dp[i] = matrix[i]; // ❌ 直接赋值 matrix,会修改原数组!
}
正确逻辑:
- dp 是独立数组,不能直接等于 matrix
- 循环应该遍历
m(行数),不是n(列数)
错误 2:初始化第一行时,把 (0,0) 重复赋值了
虽然不影响结果,但不规范,我帮你修正。
错误 3:当 matrix [i][j] = '0' 时
你没有把 dp [i][j] 设为 0 !它会继承原 matrix 的字符 '0',导致 min 计算出 NaN!
修复后的完整正确代码
javascript
/**
* @param {character[][]} matrix
* @return {number}
*/
var maximalSquare = function(matrix) {
if(matrix.length == 0 || matrix[0].length == 0 ){
return 0 ;
}
let m = matrix.length; // 行数
let n = matrix[0].length; // 列数
// ✅ 正确初始化 dp 数组(独立数组)
const dp = new Array(m);
for (let i = 0; i < m; i++) { // 循环 m 次,不是 n!
dp[i] = new Array(n).fill(0); // 新建数组,不污染原矩阵
}
let maxSize = 0;
// ✅ 初始化第一列
for (let i = 0; i < m; i++) {
if (matrix[i][0] === '1') {
dp[i][0] = 1;
maxSize = 1;
}
}
// ✅ 初始化第一行(跳过(0,0))
for (let j = 1; j < n; j++) {
if (matrix[0][j] === '1') {
dp[0][j] = 1;
maxSize = 1;
}
}
// ✅ 核心 DP 循环
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
if (matrix[i][j] === '1') {
dp[i][j] = Math.min(
dp[i-1][j],
dp[i][j-1],
dp[i-1][j-1]
) + 1;
maxSize = Math.max(maxSize, dp[i][j]);
} else {
dp[i][j] = 0; // ✅ 必须写!0 位置要置 0
}
}
}
return maxSize * maxSize;
};
最关键的 2 个坑
1)JavaScript 二维数组不能直接写 dp[i] = matrix[i]
这是引用赋值,不是拷贝!改 dp 等于改原 matrix,直接导致计算全错!
必须这样:
javascript
dp[i] = new Array(n).fill(0);
2)循环条件:i < m 行数,j < n 列数
你之前写了
javascript
for(let i=0; i < n; i++)
直接越界,程序崩溃!
一句话总结你错在哪
- dp 数组初始化方式错误
- 循环条件 m/n 写反
- 0 没有手动赋值,导致数字和字符混算
你现在把修复版代码复制过去,直接 AC ✅
需要我带你逐行对比你写的和正确版吗?我可以一步步给你讲!
3 题目
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
示例 4:
输入:prices = [1]
输出:0
提示:
1 <= prices.length <= 1050 <= prices[i] <= 105
4 代码实现
c++
cpp
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if ( n <= 1 ){
return 0 ;
}
int buy1 = -prices[0];
int sell1 = 0 ;
int buy2 = -prices[0];
int sell2 = 0 ;
for (int i = 1 ; i < n ; i ++){
int new_buy1 = max (buy1 , -prices[i]);
int new_sell1 = max (sell1 , buy1 + prices[i]);
int new_buy2 = max (buy2 , new_sell1 - prices[i]);
int new_sell2 = max (sell2 , buy2 + prices[i]);
buy1 = new_buy1 ;
sell1 = new_sell1 ;
buy2 = new_buy2 ;
sell2 = new_sell2 ;
}
return sell2 ;
}
};
js
javascript
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
const n = prices.length ;
if (n <= 1 ){
return 0 ;
}
let buy1 = -prices[0] ;
let sell1 = 0 ;
let buy2 = -prices[0];
let sell2 = 0 ;
for (let i = 1; i < n; i++) {
const newBuy1 = Math.max(buy1, -prices[i]);
const newSell1 = Math.max(sell1, buy1 + prices[i]);
const newBuy2 = Math.max(buy2, sell1 - prices[i]);
const newSell2 = Math.max(sell2, buy2 + prices[i]);
buy1 = newBuy1;
sell1 = newSell1;
buy2 = newBuy2;
sell2 = newSell2;
}
return Math.max(sell1, sell2);
};
思考
看到这个hard难度我就被吓一大跳啊hhhh...
这咋做啊,这个题目和dp有什么关系,说实话我都不能在数学上找出好的解法,好难。
题解
别怕!这道 Hard 题的核心逻辑非常固定 ,就是动态规划 + 状态机 ,我带你从 0 到 1 完全吃透,看完你会发现它一点都不难!
先给你吃定心丸:这道题不是考数学猜价格 ,而是考有限状态的动态规划 ,只要记住4 个核心状态,直接套模板!
一、先读懂题目(关键!)
- 最多可以完成 2 笔交易(可以 1 笔、0 笔,但不能超过 2 笔)
- 不能同时持有多只股票(买之前必须先卖)
- 求最大利润
二、核心思考:为什么用动态规划?
因为每一天你都有几种明确的状态,状态之间可以转移:
- 没买过股票
- 买了第 1 次,还没卖
- 卖了第 1 次,空仓
- 买了第 2 次,还没卖
- 卖了第 2 次,空仓
最终答案一定是「空仓」状态(持有股票不可能利润最大)
这就是状态机 DP ------ 把每天的行为拆成固定状态,用 DP 记录每个状态的最大利润。
三、定义 4 个核心 DP 状态(最重要!)
我们只需要 4 个变量 就能搞定:
- buy1 :第 i 天完成第 1 次买入后,手里的最大利润
- sell1 :第 i 天完成第 1 次卖出后,手里的最大利润
- buy2 :第 i 天完成第 2 次买入后,手里的最大利润
- sell2 :第 i 天完成第 2 次卖出后,手里的最大利润
✅ 最终答案:sell2(因为卖完 2 次一定是最大利润;卖 1 次也会被包含在 sell2 里)
四、状态转移方程(直接背!)
cpp
buy1 = max( 之前的buy1, -prices[i] )
sell1 = max( 之前的sell1, buy1 + prices[i] )
buy2 = max( 之前的buy2, sell1 - prices[i] )
sell2 = max( 之前的sell2, buy2 + prices[i] )
我给你逐句翻译成人话:
1. buy1(第一次买入)
- 要么:之前已经买好了,今天不动
- 要么:今天第一次买 → 利润 = 花掉 prices [i] →
-prices[i]
2. sell1(第一次卖出)
- 要么:之前已经卖好了,今天不动
- 要么:今天卖出第一次买的 → 利润 =
buy1 + 今天价格
3. buy2(第二次买入)
- 要么:之前已经买好第二次了,今天不动
- 要么:用第一次卖出的钱,今天再买 →
sell1 - 今天价格
4. sell2(第二次卖出)
- 要么:之前已经卖好第二次了,今天不动
- 要么:卖出第二次买的 →
buy2 + 今天价格
五、初始化(第一天怎么来?)
第 0 天(第一天):
- buy1 = -prices[0] (花钱买入)
- sell1 = 0 (不能当天卖,利润 0)
- buy2 = 极小值(还没完成第一笔,不能买第二笔)
- sell2 = 0 (利润 0)
C++ 用:
cpp
int buy1 = -prices[0];
int sell1 = 0;
int buy2 = INT_MIN;
int sell2 = 0;
六、C++ 完整代码(最优解!)
cpp
#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n <= 1) return 0;
// 定义4个状态
int buy1 = -prices[0]; // 第一次买入
int sell1 = 0; // 第一次卖出
int buy2 = INT_MIN; // 第二次买入
int sell2 = 0; // 第二次卖出
// 从第2天开始遍历
for (int i = 1; i < n; i++) {
// 必须用【上一轮的旧值】更新,所以先存临时变量
int new_buy1 = max(buy1, -prices[i]);
int new_sell1 = max(sell1, buy1 + prices[i]);
int new_buy2 = max(buy2, sell1 - prices[i]);
int new_sell2 = max(sell2, buy2 + prices[i]);
// 更新为新值
buy1 = new_buy1;
sell1 = new_sell1;
buy2 = new_buy2;
sell2 = new_sell2;
}
// 答案一定是 sell2(包含0次、1次、2次交易的最大值)
return sell2;
}
};
// 测试
int main() {
vector<int> prices = {3,3,5,0,0,3,1,4};
Solution sol;
cout << sol.maxProfit(prices) << endl; // 输出 6 ✅
return 0;
}
七、为什么要存临时变量?(超级关键!)
你会发现我写了 new_buy1 这种临时变量:
cpp
new_buy1 = max(buy1, -prices[i])
不能直接写:
cpp
buy1 = max(buy1, -prices[i]);
sell1 = max(sell1, buy1 + prices[i]); // ❌ 这里buy1已经被更新了!
因为:今天的所有状态,都必须用【昨天的状态】计算。直接覆盖会导致数据错乱!
八、复杂度(完美通过 1e5 数据)
- 时间:O(n) 遍历一次数组
- 空间:O(1) 只用 4 个变量。这是最优解法,没有之一!
九、带你手动算一遍示例 1(秒懂)
prices = [3,3,5,0,0,3,1,4]
最终:sell1 = 3,sell2 = 3 + 3 = 6。和题目答案完全一致!
十、你之前的困惑解答
你说:
不知道和 DP 有什么关系,数学上也想不出
原因:**这不是找规律,是状态枚举!**把每天的所有可能状态都记录下来,取最大值,就是动态规划。
超级总结(你一定能记住)
- 最多 2 次交易 → 4 个状态
- buy1 → sell1 → buy2 → sell2
- 转移方程固定不变
- 答案 = sell2
这道 Hard 题,背会这 4 个状态,直接秒杀!
5 小结
ai题解含量极高,我觉得动规还是有点难,但是必须要抓紧刷了。比较要紧...
哎,思维好混乱啊。