头像

撕得失败的标签




离线:1小时前


最近来访(6)
用户头像
Mr.dog
用户头像
那天黎明不止大雾
用户头像
yao4246
用户头像
假笑扮从容

分享 背包问题

背包问题


贪心算法

在第一次遇到背包问题的时候,我相信很多人的想法和我一样,就是使用贪心算法,我们只需要算出每件物品的性价比,也就是物品权重(价值/重量),然后排序,从高向低选做局部最优解,由此得出整体最优解,求出最大价值

这种方法对于可拆分背包当然可以,但是对于01背包,貌似就没那么简单了。

举个例子:求 $N~=~3,~V~=~15,~v~=~[~6,~7,~10~],~w~=~[~8,9,15~]$ 时的最优解,如果使用贪心那么得出来的答案会是 $w~[~2~]~=~15$,显然答案是错误的,应该是 $w~[~0~]~+~w~[~1~]~=~17$。那为什么呢?

因为对于贪心算法,他只关注性价比忽视了背包的空间,在空间上的浪费就会增加,导致单位体积物品的价值降低了,最后反而并不是整体最优解。

对于这种问题,我们使用动态规划算法来解决(当然暴力枚举也可以,这里不考虑),动态规划与贪心算法其实很相似,他们唯一的不同在于:贪心每次只做一个局部最优决策,每往下的决策越少;而动态规划则是重叠子问题,将原始问题分成若干个相关子问题。虽然都是通过获得每一个最优子结构来获得整体最优。但是这么对比起来,贪心是不是就显得有点鼠目寸光了。而动态规划则是统筹全局呢。


动态规划

那么现在我们来学习动态规划的使用,首先我们要知道,既然叫动态规划,那么他一定是动态的,同时他的每个状态依赖于前一次的状态。为了方便描述这种状态,现在我们可以给他一个二维表:dp[N][V]

状态:dp[i][j]  表示选择前 i 个物品,体积为 j 时的最优方案
状态转移方程:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])  (_当作递推公式看就行了_)
dp[i - 1][j]:上一个状态体积为 j 的价值
dp[i - 1][j - v[i]] + w[i]:上一状态体积为 {j - v[i] (第 i 件物品体积)}的价值 + w[i](第 i 件物品的价值)

这样我们就可以看到每个状态下各种体积的最优解。


$01$背包

问题描述

有 $N$ 件物品和一个容量是 $V$ 的背包。每件物品只能使用一次。

第 $i$ 件物品的体积是 $v_i$,价值是 $w_i$。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

数据范围

$0<N,V≤1000$
$0<v_i,w_i≤1000$


算法:动态规划二维表

时间和空间复杂度: $~O(n·m)$

现在我们用上述方法解决 $01$背包问题。

核心代码

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
        // 延续上一状态 
        dp[i][j] = dp[i-1][j];
        if (j >= v[i])
            dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]); // 状态转移 
    }
}

好了,这样就可以解决 $01$ 背包的简单问题了。

不过,到这里还没有结束,如果真的理解了动态规划,那么应该很容易发现,上述代码是可以优化的。

我们每多一个物品做状态转移,只需要上一次的全部状态,并不需要之前所有状态;所以我们可以将二维表转化为一维表来节省空间的使用。空间复杂度降到$~O(m)$

注意,这里的j循环是从高位到低位,因为dp[j-v[i]]表示的是容量为j-v[i]的背包装前i-1个物品的最大价值。物品容量i最大价值循环,从高位到低位同一物品至多拿一次;反之,则会出现多次。

算法:动态规划一维表

C++ 代码

#include <bits/stdc++.h>
using namespace std;
#define N 1007
int n, m, v, w, dp[N];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> v >> w;
        for (int j = m; j >= v; j--) {
            dp[j] = max(dp[j], dp[j-v] + w);
        }
    }
    cout << dp[m];
    return 0;
}

现在 $01$ 背包问题结束了,我们来看完全背包问题。


完全背包

完全背包是在 $01$ 背包的基础上将每种物品数量变为 $∞$ 大,而不是只能取一个了。

思路调整

还记得我们之前 $01$ 背包的状态转移方程吗?

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])

现在我们对他进行延申。

dp[i][j]        = max(dp[i - 1][j - k * v[i]] + k * w[i]) (k = 0, 1, ··· , n)
dp[i][j - v[i]] = max(dp[i - 1][j - k * v[i]] + k * w[i]) (k = 1, 2, ··· , n)

将上述两式相减就可以得出完全背包的状态转移方程了。

dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i])

同 $01$ 背包一样,我们将二维表进行优化得出代码:

C++ 代码

#include <bits/stdc++.h>
using namespace std;
#define N 1007
int n, m, v, w, dp[N];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> v >> w;
        for (int j = v; j <= m; j++) {
            dp[j] = max(dp[j], dp[j-v] + w);
        }
    }
    cout << dp[m];
    return 0;
} 

注意这里完全背包与 $01$ 背包的的区别!!!我们现在将两个方程放在一起,并对核心代码进行比较。

状态转移方程

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]) // 01背包
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i])   // 完全背包

核心代码

// 01背包
for (int i = 1; i <= n; i++) 
for (int j = m; j <= v[i]; j++) // 从高位到低位
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
// 完全背包
for (int i = 1; i <= n; i++) 
for (int j = v[i]; j <= m; j++) // 从低位到高位
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);

还记得之前二维表优化为一维表的方法吗?在这里我们也可以这样理解,dp[j-v[i]]表示的是容量为j-v[i]的背包装前i-1个物品的最大价值。物品容量i最大价值循环,从高位到低位同一物品至多出现一次,而从低位到高位则同一物品可以出现多次。


学完 $01$ 背包和完全背包,趁着脑子还热,我们继续来学习一下多重背包问题吧!!!先来看看问题描述。


多重背包问题 I

问题描述

有 $N$ 件物品和一个容量是 $V$ 的背包。

第 $i$ 件物品最多有 $s_i$ 件,每件体积是 $v_i$,价值是 $w_i$。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

数据范围

$0<N,V≤100$
$0<v_i,w_i,s_i≤100$

算法:动态规划 + 暴力

时间复杂度: $~O(m·n·s)$

我们先来看一下多重背包问题的状态转移方程,用 $k$ 表示每个物品选中次数,

f[i][j] = max(f[i - 1][j - k * v[i]] + k * w[i]) (k = 0, 1, ···, s[i])

想要得到最终价值并不难,我们只要在 $01$ 背包的 $j$ 循环下加个 $k$ 循环,循环给出两个条件:

$1)~k~≤~s$
$2)~k~*~v~≤~j$

这样就可以得出最大价值了。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
#define N 107
int n, m, v, w, s, dp[N];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) { 
        cin >> v >> w >> s;
        for (int j = m; j >= v; j--) {
            for (int k = 1; k <= s && k * v <= j; k++) {
                dp[j] = max(dp[j], dp[j - k * v] + k * w);
            }
        }
    }
    cout << dp[m];
    return 0;
} 

这样就结束了吗?并没有,如果我们将范围扩大到:$0<N≤1000$,$0<V≤2000$,$0<v_i,w_i,s_i≤2000$ 会怎么样呢?显然 $n·m·s~>~10^9$,这样提交一定是会 $TLE$ 的。那么我们该怎么优化呢?


多重背包问题 II

数据范围变为:

$0<N≤1000$
$0<V≤2000$
$0<v_i,w_i,s_i≤2000$

算法:动态规划 + 二进制

时间复杂度: $~O(m·n·logs)$

原来的做法是,对于每件物品,我们一次拿一个。现在我们把每种物品的总件数 $s$,分成一个又一个堆,我们每次都拿其中的一个堆,那么我们该怎么样分配呢?

我们可以利用二进制的性质对其进行优化,比如我们现在有 $7$ 件物品,原来我们最多可能要拿 $7$次,现在我们使用二进制换算 $7B~=~111$,我们可以把他拆解成 $100~010~001$ 这三个堆,他们可以组合成任意 $≤7$ 的数,而且每种组合都会得到不同的数,那么我至多只需要拿 $3$ 次就可以拿完。

假设我要拿 $6$ 件,原来我们一个一个拿,需要拿 $6$ 次才能全部取完。现在我只需要拿 $2$ 次,我们只要拿 $~2,~4$ 这两堆就可以了。这样就可以将时间复杂度 $~O(m·n·s)$ 降到了 $~O(m·n·logs)$,大概从 $4·10^9$ 降到了 $2·10^7$,可以通过。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
#define N 2007
#define M 12007

// 多重背包问题 
// 二进制优化 

int n, m, vi, wi, si, k, ans;
int w[M], v[M], dp[N];

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> vi >> wi >> si;
        // 进行二进制拆解,每次左移一位 
        for (int k = 1; k <= si; k <<= 1) {
            w[++ans] = wi * k; // 这一位的全部价值 
            v[ans] = vi * k;   // 这一位的全部体积 
            si -= k; // 减去这一位的全部 
        }
        // 最后一位放剩余部分 
        if (si > 0) {
            w[++ans] = wi * si;
            v[ans] = vi * si;
        }
    }
    // 因为已经进行了二进制优化,所以此时 n 变为 ans 
    for (int i = 1; i <= ans; i++) {
        for (int j = m; j >= v[i]; j--) {
            dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
        }
    }
    cout << dp[m];
    return 0;
}


思考?

在 $01$ 背包中我们对【空间】进行了优化,空间复杂度从 $~O(n·m)$ 降到了 $~O(m)$;

在多重背包中我们利用二进制思想

对同种【物品的分类】进行了优化,时间复杂度从 $~O(m·n·s)$ 降到了 $~O(m·n·logs)$;

到此已经是最优了吗?我们还可以对哪些部分进行优化?




题目描述

blablabla

样例

blablabla

算法1

为了方便观察,将杨辉三角居左展示,并写出 $11$ 行内容;

1  ---> c[0][0]
1       1  ---> c[1][1]
1       2       1
1       3       3       1
1       4       6       4       1
1       5       10      10      5       1
1       6       15      20      15      6       1
1       7       21      35      35      21      7       1
1       8       28      56      70      56      28      8       1
1       9       36      84      126     126     84      36      9       1
1       10      45      120     210     252     210     120     45      10      1

通过观察容易发现在第 $i$ 行的数所对应的数为 $(a+b)^i$ 二项式展开系数;

由此可以得出结论:

$C~[~i~]~[~j~]~=~C^j_i$

第 $i$ 行最大值为 $C~[~i~]~[~i~/~2~]~=~C^{i~/~2}_i$

又因为其左右对称所以我们只用看最大值左边内容;

我们从第 $0$ 行开始:

要找 $n$ 我们可以先定位到第 $i$ 行的最大值 $C~[~i~]~[~i~/~2~]~>~n$

假设 $j~=~i~/~2,sum=C~[~i~]~[~j~]$,从第 $i$ 行第 $j$ 列向左移动;

当 $sum~<~n$ 时,向下移动直到 $sum~>~n$,重复上述步骤

如果上述过程中出现 $C~[~i~]~[~2~]~>~n$,

显然第 $2$ 列下方一定都大于 $n$,那么 $n$ 第一次出现一定在第 $n$ 行第 $1$ 列;

假设 $n = 30$ 流程如图:
QQ图片20230321231921.png

找到数 $n$ 第一次出现的位置为 $C~[~i~]~[~j~]$, 显然从第 $0$ 行到第 $i-1$ 行上共 $f(i)=\frac{n~*~(~n~+~1~)}{2}$ 个数;

那么就是第一次出现 $n$ 是在第 $f(i)~+~j$ 个数;

注意:在求组合数的时候我们可以边乘边除,如果将分子分母单独求出,分子可能会出现越界的情况。

C++ 代码

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

ll fun(int i, int j) {
    int k = j, m = 1;
    ll a = 1;
    while (k--) {
        a *= i--;
        a /= m++;
    }
    return a;
}

int main() {
    ll n, i = 0, j, sum = 0;
    cin >> n;
    while (sum < n) {
        sum = fun(i, i / 2);
        if (sum == n) {
            j = i / 2 + 1;
            while (i) j += i--;
            cout << j;
            return 0;
        }
        i++;
    }
    i--;
    j = i / 2 - 1;
    while (j) {
        if (n < fun(i, 2))
            break;
        sum = fun(i, j);
        if (sum == n) {
            while (i) j += i--;
            cout << j + 1;
            return 0;
        }
        if (sum < n) i++;
        else j--;
    }
    cout << n * (n + 1) / 2 + 2;
    return 0;
}



题目描述

blablabla

样例

blablabla

算法1:动态规划( $01$ 背包问题 )

时间复杂度 $O(n·sum)$

和 $01$ 背包问题相似,我们给他一个二维表:dp[N][M]

状态:dp[i][j]  表示选择前 i 个砝码,重量为 j 是否存在;
状态转移方程:dp[i][j] = dp[i - 1][j] || dp[i - 1][abs(j - w[i])] || dp[i - 1][j + w[i]]);

核心代码

for (int i = 1; i <= n; i++) {
    for (int j = 0; j <= sum; j++) {
        dp[i][j] = dp[i - 1][j] || dp[i - 1][abs(j - w[i])] || dp[i - 1][j + w[i]];
    }
}

显然我们可以将二维表优化成一维表实现;

注意,状态转移中出现的最大重量为 j + w,所以 $M$ 应给 $2·10^5~+~7$

C++ 代码

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

int v[M], n, ans, sum, w;
bool dp[M];

int main() {
    cin >> n;
    dp[0] = true;
    for (int i = 1; i <= n; i++) {
        cin >> w;
        sum += w;
        int k = 0;
        for (int j = 1; j <= sum; j++)
            if (dp[abs(j - w)] || dp[j + w]) v[++k] = j;
        while (k) dp[v[k--]] = true;
    }
    for (int i = 1; i <= sum; i++)
        if (dp[i]) ans++;
    cout << ans;
    return 0;
}




题目描述

blablabla

样例

blablabla

算法

!!!$1s = 1000ms$ !!!

C++ 代码

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

int main() {
    ll n;
    int h, m, s;
    cin >> n;
    n /= 1000;
    h = n / 3600 % 24;
    m = n / 60 % 60;
    s = n % 60;
    printf("%02d:%02d:%02d", h, m, s);
    return 0;
}


活动打卡代码 AcWing 4873. 简单计算

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

int main() {
    int x1, x2, y1, y2;
    cin >> x1 >> x2 >> y1 >> y2;
    cout << max(abs(x1 - x2), abs(y1 - y2));
    return 0;
}



题目描述

blablabla

样例

blablabla

算法

从第 $1$ 颗竹子一直砍到第 $n$ 颗,并记录每颗竹子被砍过程中所出现的所有情况,直到高度为 $1$,并记录次数;

当第 $i$ 颗竹子被砍的过程中高度 $x$,存在于第 $i-1$ 颗竹子被砍的过程中,则不计数。

注意:高度最大值为 $10^{18}$,开方的时候使用 $sqrtl$ 函数。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 200007

ll ans;
vector<ll> vec[N];

ll doIt(ll x) {
    // 数据值过大使用 sqrtl; 
    return sqrtl(x / 2 + 1);
}

int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        // 记录第 i 颗竹子高度; 
        ll x;
        cin >> x;
        // 砍第 i 颗竹子; 
        while (x > 1) {
            // 判断第 i 颗竹子砍的过程中,是否与第 i-1 颗存在公共值,默认为 false; 
            bool flag = false;
            for (auto it : vec[i-1])
                // 第 i-1 颗竹子被砍的过程中存在 x 时,flag = true; 
                if (it == x)
                    flag = true;
            // 不存在公共值次数加一; 
            if (!flag) ans++;
            // 记录第 i 颗被砍过程中存在的高度; 
            vec[i].push_back(x);
            // 砍竹子; 
            x = doIt(x);
        }
    }
    cout << ans;
    return 0;
} 



题目描述

blablabla

样例

blablabla

算法1: DFS

时间复杂度: $O(2^n)$

本题最易想到的就是深搜,枚举所有,用几种规则进行剪枝,排除所有不合法的情况。

用 $k$ 表示酒的量;

已知:最后一次遇到的必须是花,并且没酒时遇花是不合法的;

当 $n=0$ 时,只有 $k=m$的时候有解,否则无解;

当 $k=0$ 时,只有 $m=n=0$ 的时候有解,否则无解;

当 $k≠0$ 且 $n≥m$ ,或者 $k~>~m$ 时,无解;

单纯的深搜时间复杂度为 $O(2^n)$,当 $n≥30$ 时就容易超时,只能拿部分分。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mod 1000000007

ll dfs(int n, int m, int k) {
    if (n == 0) return (k == m);
    if (k == 0) return (n == 0 && m == 0);
    if (n >= m || k > m) return 0;
    ll ans = dfs(n, m-1, k-1) + dfs(n-1, m, k*2);
    return ans % mod;
}

int main() {
    ll ans;
    int n, m;
    cin >> n >> m;
    ans = dfs(n, m, 2);
    cout << ans;
    return 0;
}

算法2: 记忆化搜索

时间复杂度: $O(n^2)$

使用 $dp[n][m][k]$ 表示遇到 $n$ 次店,$m$ 次花,剩下 $k$ 斗酒,记录此时可能种数;
因为 $n,m ≤ 100$,所以当 $k > 100$ 时,不合法,故给 $dp$ 空间为 $107·107·107$。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mod 1000000007
#define N 107

ll dp[N][N][N];

ll dfs(int n, int m, int k) {
    if (n == 0) return (k == m);
    if (k == 0) return (n == 0 && m == 0);
    if (n >= m || k > m) return 0;
    if (dp[n][m][k]) return dp[n][m][k];
    ll ans = dfs(n, m-1, k-1) + dfs(n-1, m, k*2);
    dp[n][m][k] = ans;
    return ans % mod;
}

int main() {
    ll ans;
    int n, m;
    cin >> n >> m;
    ans = dfs(n, m, 2);
    cout << ans;
    return 0;
}



题目描述

blablabla

样例

blablabla

算法1: 贪心

由题可知:对于 $X$ 进制数 $321$,进制分别为 $8~10~2$,转化为十进制数为 $65$。
算式:$3·10·2~+~2·2~+~1~=~65$

假设, $A$ 和 $B$ 都是 $n$ 位数,分别是 $a_na_{n-1}···a_1$ 和 $b_nb_{n-1}···b_1$,对应的进制数 $X$ 分别是 $p_np_{n-1}···p_1$

由此可知:
$A$ 的十进制应为:$a_n·(p_{n-1}···p_1)~+~a_{n-1}·(p_{n-2}···p_1)~+···+~a_1$,
$B$ 的十进制应为:$b_n·(p_{n-1}···p_1)~+~b_{n-1}·(p_{n-2}···p_1)~+···+~b_1$,

所以,$A - B = (a_n-b_n)·(p_{n-1}···p_1)~+~(a_{n-1}-b_{n-1})·(p_{n-2}···p_1)~+···+~(a_1 - b_1)$

判断当 $a_n~≥~b_n$ 时,对应的进制数 $x_n$ 都尽可能的取小(最小为 $2$ ),就可以得出 $A−B$ 的最小值

不过如果 $a_n~<~b_n$ 时,$x_n$是否依然是尽可能取小呢,答案是肯定的。

如果 $a_n~<~b_n$,我们让 $x_{n-1}$ 变大来使得最终值变小,假设在原 $x_{n-1}$ 的基础上增加 $x$ 那么在此位的值将减小 $(b_n-a_n)·(x·x_{n-2}···x_1)$,

注意到,题目在最后数据范围中给出 $A ≥ B$ 条件,说明如果存在 $a_n < b_n$ 的情况,那么在更高位一定存在 $a_m > b_m$,相应的,因为比 $n$ 更高位的每个数都要乘上这个$x_{n-1}$, 所以,想要使更高位的增值最小的情况是 $m = n + 1$,增值为 $(a_{n+1}-b_{n+1})·(x_n·x·x_{n-2}···x_1)$

将两次变化值相加得:$[~(a_{n+1}-b_{n+1})·x_n-(b_n-a_n)~]·(x·x_{n-2}···x_1)$

显然,$(x·x_{n-2}···x_1)~>~0$,只需要判断 $(a_{n+1}-b_{n+1})·x_n-(b_n-a_n)$ 的正负。

因为,$a_{n+1}~>~b_{n+1}$ 并且都为正整数,所以,$(a_{n+1}-b_{n+1})~≥~1$

又因为, $n$ 进制一定大于 $n$ 进制每一位的数,所以,$x_n>b_n>b_n-a_n$

所以,$(a_{n+1}-b_{n+1})·x_n-(b_n-a_n)~>~0$

由此可知,最终值一定变大,故当$a_n~<~b_n$ 时,$x_n$依然是要尽可能取小

C++ 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mod 1000000007
#define N 100010

int n, ma, mb;
int a[N], b[N];
ll ans = 0, xn = 1;

int main() {
    cin >> n >> ma;
    for (int i = 1; i <= ma; i++) cin >> a[i];
    cin >> mb;
    for (int i = 1; i <= mb; i++) cin >> b[i];
    int i = ma, j = mb;
    while (i > 0) {
        ans += (a[i] - b[j]) * xn;
        ans %= mod;
        ll x = max(a[i], b[j]) + 1;
        xn *= max(x, 2ll);
        xn %= mod;
        i--;
        if (j) j--;
    }
    cout << ans;
    return 0;
}





题目描述

blablabla

样例

blablabla

算法1: 前缀和 + 暴力枚举

时间复杂度 $O(n^4)$

使用前缀和进行暴力枚举,枚举每个点为起点,和枚举每个点为终点。时间复杂度是 $O(n^4)$。
对于 $70\%$ 的数据,$N,M≤100$,大概为 $10^8$,可以通过;
对于 $100\%$ 的数据,$1≤N,M≤500$,大概为 $10^{10}$,部分会超时;
暴力大概可以获得 $70$% 的分数。

C++ 代码

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

long long a[502][502];

int main()
{
    int n, m, k;
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            cin >> a[i][j];
            a[i][j] += a[i-1][j];
        }
    }
    long long ans = 0;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            long long s = a[i][j], row = i, col = j, temp = a[i-1][j];
            while (s - temp <= k && row <= n)
            {
                while (s - temp <= k && col <= m)
                {
                    ans++;
                    col++;
                    s += a[row][col];
                    temp += a[i-1][col];
                }
                col = j;
                row++;
                temp = a[i-1][j];
                s = a[row][j];
            }
        }
    }
    cout << ans;
    return 0;
}

算法2: 前缀和 + 双指针滑动窗口

时间复杂度 $O(n^3)$

从枚举每个点转换为枚举每一列,以每一列为起点和终点,使用双指针滑动窗口思维;
用 $left$ 表示左边界,从 $1$ 开始,$right$ 表示右边界,也从 $1$ 开始,子矩阵和为 $s$;
当 $s≤k$ 时,子矩阵的个数为 $right - left + 1$ 个矩阵;
当 $s>k$ 时, $left$ 就向右移动,一直移动到满足 $s≤k$ 的位置,当 $left$ 走到 $m$ 点时循环结束。
对于 $100\%$ 的数据,$1≤N,M≤500$,大概为 $10^8$,可以全部通过。

C++ 代码

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

long long a[502][502];

int main()
{
    int n, m, k;
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            cin >> a[i][j];
            a[i][j] += a[i-1][j];
        }
    }
    long long ans = 0;
    for (int i = 1; i <= n; i++)
    {
        for (int j = i; j <= n; j++)
        {
            long long left = 1, right = 1, s = 0;
            while (right <= m)
            {
                s += a[j][right] - a[i-1][right];
                while (s > k)
                {
                    s -= a[j][left] - a[i-1][left];
                    left++;
                }
                ans += right++ - left + 1;
            }
        }
    }
    cout << ans;
    return 0;
}



要摆出 $n$ 列方格,可以在 $n - 1$ ~ $n$ 列方格后加一个竖着的 $I$ 型积木,也可以在 $n - 2$ ~ $n$ 列方格后加两个横着的 $I$ 型积木,也可以在 $n - 3$ ~ $n$ 列方格上后加两个 $L$ 型积木的两种摆法, 也可以在 $n - 4$ ~ $n$ 列方格上两边加两个 $L$ 型积木中间加一个横着的 $I$ 型的两种摆法,也可以在 $n - 5$ ~ $n$ 列方格上两边加两个 $L$ 型积木中间加两个个横着的 $I$ 型的两种摆法,如图:

QQ截图20230319113900.png

以此类推,可得 $1$ ~ $n$ 列方格的摆法$dp[n]$的方程为:

$dp[n] = dp[n - 1] + dp[n - 2] + dp[n - 3] · 2 + dp[n - 4] · 2 + dp[n - 5] · 2 + ··· + dp[1] · 2$
$dp[n - 1] = dp[n - 2] + dp[n - 3] + dp[n - 4] · 2 + dp[n - 4] · 2 + dp[n - 5] · 2 + ··· + dp[1] · 2$

两式相减得:$dp[n] = dp[n-1] · 2 + dp[n-3]$

C++ 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 10000010
#define mod 1000000007

int main()
{
    ll *a, n;
    a = new ll[N];
    cin >> n;
    a[1] = 1;
    a[2] = 2;
    a[3] = 5;
    for (int i = 4; i <= n; i++)
        a[i] = (a[i-1]*2 + a[i-3])%mod;
    cout << a[n];
    return 0;
}