This commit is contained in:
HuangHai
2025-08-30 18:35:01 +08:00
commit 1f397eca87
6258 changed files with 1487241 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 10010;
int t, n, m;
int f[M];
int w[N][N];
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> t >> n >> m;
for (int i = 1; i <= t; i++)
for (int j = 1; j <= n; j++)
cin >> w[i][j];
for (int i = 1; i < t; i++) { // t-1轮背包
memset(f, 0, sizeof f);
for (int j = 1; j <= n; j++)
if (w[i + 1][j] > w[i][j]) // 优化:只有在价值为正数时才会考虑使用该物品
for (int k = w[i][j]; k <= m; k++)
f[k] = max(f[k], f[k - w[i][j]] + w[i + 1][j] - w[i][j]);
m += f[m];
}
printf("%d\n", m);
return 0;
}

View File

@@ -0,0 +1,170 @@
## [$AcWing$ $1163$. 纪念品](https://www.acwing.com/problem/content/description/1165/)
### 一、题目描述
小伟突然获得一种超能力,他知道未来 $T$ 天 $N$ 种纪念品每天的价格。
某个纪念品的价格是指购买一个该纪念品所需的金币数量,以及卖出一个该纪念品换回的金币数量。
每天,小伟可以进行以下两种交易无限次:
任选一个纪念品,若手上有足够金币,以当日价格购买该纪念品,**注意** 同一个纪念品可以在同一天重复买;
卖出持有的任意一个纪念品,以当日价格换回金币。
每天卖出纪念品换回的金币可以立即用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。
当然,一直持有纪念品也是可以的。
$T$ 天之后,小伟的超能力消失。
因此他一定会在第 $T$ 天卖出所有纪念品换回金币。
小伟现在有 $M$ 枚金币,他想要在超能力消失后拥有尽可能多的金币。
**输入格式**
第一行包含三个正整数 $T,N,M$,相邻两数之间以一个空格分开,分别代表未来天数 $T$,纪念品数量 $N$,小伟现在拥有的金币数量 $M$。
接下来 $T$ 行,每行包含 $N$ 个正整数,相邻两数之间以一个空格分隔。第 $i$ 行的 $N$ 个正整数分别为 $P_{i,1},P_{i,2},……,P_{i,N}$,其中 $P_{i,j}$ 表示第 $i$ 天第 $j$ 种纪念品的价格。
**输出格式**
输出仅一行,包含一个正整数,表示小伟在超能力消失后最多能拥有的金币数量。
**数据范围**
对于 $10\%$ 的数据,$T=1$。
对于 $30\%$ 的数据,$T≤4,N≤4,M≤100$,所有价格 $10≤P_{i,j}≤100$。
对于 $15\%$ 的数据,$T≤100,N=1$。
对于 $15\%$ 的数据,$T=2,N≤100$。
对于 $100\%$ 的数据,$T≤100,N≤100,M≤10^3$,所有价格 $1≤P_{i,j}≤10^4$,数据保证任意时刻,小明手上的金币数不可能超过 $10^4$。
**输入样例1**:
```cpp {.line-numbers}
6 1 100
50
20
25
20
25
50
```
**输出样例1**:
```cpp {.line-numbers}
305
```
**输入样例2**:
```cpp {.line-numbers}
3 3 100
10 20 15
15 17 13
15 25 16
```
**输出样例2**:
```cpp {.line-numbers}
217
```
**样例解释**
样例#1:
最佳策略是:
- 第二天花光所有 $100$ 枚金币买入 $5$ 个纪念品 $1$;
- 第三天卖出 $5$ 个纪念品 $1$,获得金币 $125$ 枚;
- 第四天买入 $6$ 个纪念品 $1$,剩余 $5$ 枚金币;
- 第六天必须卖出所有纪念品换回 $300$ 枚金币,第四天剩余 $5$ 枚金币,共 $305$ 枚金币。
超能力消失后,小伟最多拥有 $305$ 枚金币。
样例#2:
最佳策略是:
- 第一天花光所有金币买入 $10$ 个纪念品 $1$;
- 第二天卖出全部纪念品 $1$ 得到 $150$ 枚金币并买入 $8$ 个纪念品 $2$ 和 $1$ 个纪念品 $3$,剩余 $1$ 枚金币;
- 第三天必须卖出所有纪念品换回 $216$ 枚金币,第二天剩余 $1$ 枚金币,共 $217$ 枚金币。
超能力消失后,小伟最多拥有 $217$ 枚金币。
### 二、算法解析
**(贪心,$DP$) $O(TNM)$**
**关键点**
> 在某一种方案中,如果我们在第$i$天买入第$j$天卖出,其中 $i<j$,则等价于在第 $i$天买入,第 $i+1$ 天卖出,第 $i+1$ 天再买入, …, 在第 $j$ 天卖出。
> <font color='red' size=4><b>解读</b></font>:比如纪念品$A$,我们假设最优策略是第$1$天买入,第$7$天卖出是最优的,可以选择的等价策略是第$1$天买入,第$2$天卖出,第$2$天再买入,第$3$天卖出,...,第$7$天卖出,这与我们的原始策略是一样的。
> 按数学思路来说明一下,第$1$天买入,第$7$天卖出=$P_7-P_1$
> 第$1$天买入,第$2$天卖出,第$2$天买入,第$3$天卖出,...第$7$天卖出$=-P_1+P_2-P_2+P_3-P_3+P_4-P_4+P_5-P_5+P_6-P_6+P_7=P_7-P_1$
> 两者是一样的。
这样的话,所有交易不会跨两天,因此目标就变成了 **贪心策略**:先尽可能使第二天的钱最多,再尽可能使第三天的钱最多,依次类推直到最后一天。
> <font color='red' size=4><b>解读</b></font>:这个为什么是正确的呢?
> 可以用反证法:如果我第$2$天亏一些,有没有可能让第$3$天更赚呢?
> 这是不可能的,比如第$2$天我剩下的钱是可能是$m_1$个金币,也可能是$m_2$个金币,而且满足$m_1<m_2$,那么$m_2$一定比$m_1$要好,这是显然的,为什么呢?
因为我在购买下一天的纪念品的时候,肯定是手中的钱越多越多,因为如果钱少,那么可能有些纪念品我就买不起,就会少一些机会,结果不可能变得更好。$m_1$块钱可以购买的所有纪念品,$m_2$块钱肯定是可以购买到的,反过来就不是了。$m_2$考虑的所有方案,一定包含$m_1$考虑的所有方案,所以$m_2$考虑所有方案的最大值一定比$m_1$考虑的所有方案的最大值要大。所以,得到结论:我们每天都要争取最大值,这样,最终就是最大值。
>
>**注:类似于邻项交换的贪心策略**
接下来的问题变成:
> 假设第 $i$ 天的钱数是 $m_i$,那么第 $i+1$ 天的钱数最多是多少呢?
这是一个经典的 **完全背包** 问题:
- 背包容量是 $m_i$;
- 每个股票都是一种物品,体积是第 $i$ 天买入的价格,价值在第 $i$ 天买入第 $i+1$ 天卖出的收益;
每天可以进行无限次交易,因此每种物品都是可以用无限次;
那么第 $i+1$ 天的最大收益 $m_{i+1}$ 就是 $m_i$ 加上背包模型所求出的最大收益。
本题时间复杂度恰好是 $10^8$,比较极限,因此可以加一些优化:
- 只有当某支股票的收益为正数时,才考虑使用该物品。
**时间复杂度**
一共有 $T≤100$ 天,每天有 $N≤100$ 支股票,每天的钱数最大是 $M≤10000$。
算法中一共会算 $T−1$ 次背包模型,每算一次的时间复杂度是 $O(NM)$,因此总时间复杂度是 $O(TNM)=10^8$。
### 三、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 10010;
int t, n, m;
int f[M];
int w[N][N];
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> t >> n >> m;
for (int i = 1; i <= t; i++)
for (int j = 1; j <= n; j++)
cin >> w[i][j];
for (int i = 1; i < t; i++) { // t-1轮背包
memset(f, 0, sizeof f);
for (int j = 1; j <= n; j++)
if (w[i + 1][j] > w[i][j]) // 优化:只有在价值为正数时才会考虑使用该物品
for (int k = w[i][j]; k <= m; k++)
f[k] = max(f[k], f[k - w[i][j]] + w[i + 1][j] - w[i][j]);
m += f[m];
}
printf("%d\n", m);
return 0;
}
```

View File

@@ -0,0 +1,46 @@
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
int g[N][N]; // 原始地图
// 1e3 * 1e3 * 1e4 开longlong
LL f1[N][N]; // 向右+向下走
LL f2[N][N]; // 向右+向上走
int n, m;
/*
现象:
实测 max( max(a,b),max(c,d)) 795 ms
max({a,b,c,d}) 1200ms
结论:
放弃max({a,b,c,d}),这个太慢了
*/
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> g[i][j];
memset(f1, -0x3f, sizeof f1); // 有负数,初始化为-INF
memset(f2, -0x3f, sizeof f2); // 有负数,初始化为-INF
f1[1][1] = f2[1][1] = g[1][1]; // 左上角的数必须拿起来
for (int j = 1; j <= m; j++) { // 第一层循环列数
// 用上一列两种问题的状态更新这一列的状态达到最优 //行数之间也要更新最优的状态
for (int i = 1; i <= n; i++) // 因为是向下走,所以从第一行开始循环
f1[i][j] = max(max(f1[i][j - 1], f2[i][j - 1]) + g[i][j], max(f1[i][j], f1[i - 1][j] + g[i][j]));
for (int i = n; i >= 1; i--) // 因为是向上走,所以从第N行开始循环
f2[i][j] = max(max(f1[i][j - 1], f2[i][j - 1]) + g[i][j], max(f2[i][j], f2[i + 1][j] + g[i][j]));
}
cout << f1[n][m] << endl;
// cout << max(f1[n][m], f2[n][m]);
// 两种问题取最优(其实不用去max因为{N, M}的格子只能从上面走下来,所以直接去f1[n][m]即可)
return 0;
}

View File

@@ -0,0 +1,154 @@
## [$AcWing$ $2770$. 方格取数](https://www.acwing.com/problem/content/2772/)
### 一、题目描述
设有 $n×m$ 的方格图,每个方格中都有一个整数。
现有一只小熊,想从图的左上角走到右下角,每一步只能向上、向下或向右走一格,并且不能重复经过已经走过的方格,也不能走出边界。
小熊会取走所有经过的方格中的整数,求它能取到的整数之和的最大值。
**输入格式**
第 $1$ 行两个正整数 $n,m$。
接下来 $n$ 行每行 $m$ 个整数,依次代表每个方格中的整数。
**输出格式**
一个整数,表示小熊能取到的整数之和的最大值。
**数据范围**
对于 $20\%$ 的数据,$n,m≤5$。
对于 $40\%$ 的数据,$n,m≤50$。
对于 $70\%$ 的数据,$n,m≤300$。
对于 $100\%$ 的数据,$1≤n,m≤1000$。
方格中整数的绝对值不超过 $10^4$。
**输入样例1**:
```cpp {.line-numbers}
3 4
1 -1 3 2
2 -1 4 -1
-2 2 -3 -1
```
**输出样例1**:
```cpp {.line-numbers}
9
```
**样例1解释**
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310100957885.png)
按上述走法,取到的数之和为 $1+2+(−1)+4+3+2+(−1)+(−1)=9$,可以证明为最大值。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310100958920.png)
注意,上述走法是错误的,因为第 $2$ 行第 $2$ 列的方格走过了两次,而根据题意,不能重复经过已经走过的方格。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310100958655.png)
另外,上述走法也是错误的,因为没有走到右下角的终点。
**输入样例2:**
```cpp {.line-numbers}
2 5
-1 -1 -3 -2 -7
-2 -1 -4 -1 -2
```
**输出样例2:**
```cpp {.line-numbers}
-10
```
**样例2解释**
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310100959223.png)
按上述走法,取到的数之和为 $(−1)+(−1)+(−3)+(−2)+(−1)+(−2)=−10$,可以证明为最大值。
因此,请注意,取到的数之和的最大值也可能是负数。
### 二、题目解析
>$70pts$ 做法
$O(n2m)$ $DP$
先考虑按 **最普通的方格取数** 问题进行 $DP$。
转移方程:
$$\large f[i][j]=max(f[i][j−1],f[i−1][j],f[i+1][j])$$
**这样做是有后效性的**,即更新完 $f[i+1][j]$ 后可能会重复更新 $f[i][j]$。
那么就要想新的做法了。
由于不能往左走,所以 **在每列中走过的序列必然是连续的一段**。
一共有 $m$ 列,所以我们走出的路径必然是 $m$ 条竖着的段,并且每一段是相邻的。
又因为不能往回走,所以在每列中,只能一直向上走或一直向下走。
> <font color='red' size=4><b>注:</b></font>**结合样例理解一下**:
> ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310100957885.png)
这个性质启发我们 **枚举列** 进行 $DP$。
**$Q$:怎么保证先前的状态不会被改变呢?**
**答**:多开一个状态。
开出来两个状态数组,表示 **只向下+向右走** 和 **只向上+向右走** 可以到达$(i,j)$这个格子能拾取到的 **最大值**,这就将问题分成了两个问题:
- 能向右走或者向下走到达$(i,j)$这个格子可以取到的最大值;(相当于从左上角开始)
- 能向右走或者向上走到达$(i,j)$这个格子可以去到的最大值;(相当于从左下角开始)
两种问题取最大值就是答案。
#### 代码实现
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
int g[N][N]; // 原始地图
// 1e3 * 1e3 * 1e4 开longlong
LL f1[N][N]; // 向右+向下走
LL f2[N][N]; // 向右+向上走
int n, m;
/*
现象:
实测 max( max(a,b),max(c,d)) 795 ms
max({a,b,c,d}) 1200ms
结论:
放弃max({a,b,c,d}),这个太慢了
*/
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> g[i][j];
memset(f1, -0x3f, sizeof f1); // 有负数,初始化为-INF
memset(f2, -0x3f, sizeof f2); // 有负数,初始化为-INF
f1[1][1] = f2[1][1] = g[1][1]; // 左上角的数必须拿起来
for (int j = 1; j <= m; j++) { // 第一层循环列数
// 用上一列两种问题的状态更新这一列的状态达到最优 //行数之间也要更新最优的状态
for (int i = 1; i <= n; i++) // 因为是向下走,所以从第一行开始循环
f1[i][j] = max(max(f1[i][j - 1], f2[i][j - 1]) + g[i][j], max(f1[i][j], f1[i - 1][j] + g[i][j]));
for (int i = n; i >= 1; i--) // 因为是向上走,所以从第N行开始循环
f2[i][j] = max(max(f1[i][j - 1], f2[i][j - 1]) + g[i][j], max(f2[i][j], f2[i + 1][j] + g[i][j]));
}
cout << f1[n][m] << endl;
// cout << max(f1[n][m], f2[n][m]);
// 两种问题取最优(其实不用去max因为{N, M}的格子只能从上面走下来,所以直接去f1[n][m]即可)
return 0;
}
```

View File

@@ -0,0 +1,47 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 10, INF = 0x3f3f3f3f;
int n, m;
int w[N], s[N];
int f[N][N][M], g[N][N][M];
int get_mod(int x) {
return (x % 10 + 10) % 10;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> w[i];
w[i + n] = w[i]; // 破环成链
}
for (int i = 1; i <= n * 2; i++) s[i] = s[i - 1] + w[i]; // 预处理前缀和,下标从1开始
memset(f, 0x3f, sizeof f); // 预求最小,先设最大
memset(g, -0x3f, sizeof g); // 预求最大,先设最小
for (int len = 1; len <= n; len++) // 区间DP,先枚举长度
for (int l = 1; l + len - 1 <= n * 2; l++) { // 枚举起点
int r = l + len - 1; // 计算终点
// DP 初始化
// 边界条件是全部划分为k=1,也就是一块时的结果是多少,根据题意,一整块当然就是区间内容相加后对10取模
f[l][r][1] = g[l][r][1] = get_mod(s[r] - s[l - 1]);
// 状态转移
for (int k = 2; k <= m; k++) // 枚举每个可以划分成的部分
for (int j = l + k - 2; j <= r - 1; j++) { // 枚举K-1部分时的最后一个终点位置
f[l][r][k] = min(f[l][r][k], f[l][j][k - 1] * get_mod(s[r] - s[j]));
g[l][r][k] = max(g[l][r][k], g[l][j][k - 1] * get_mod(s[r] - s[j]));
}
}
// 枚举每个区间长度为n的区间,获取符合长度要求的区间内,划分成m块的最大、最小值
int mx = -INF, mi = INF;
for (int i = 1; i <= n; i++) {
mx = max(mx, g[i][i + n - 1][m]);
mi = min(mi, f[i][i + n - 1][m]);
}
cout << mi << endl;
cout << mx << endl;
return 0;
}

View File

@@ -0,0 +1,154 @@
## [$AcWing$ $414$. 数字游戏](https://www.acwing.com/problem/content/416/)
### 一、题目描述
丁丁最近沉迷于一个数字游戏之中。
这个游戏看似简单,但丁丁在研究了许多天之后却发觉原来在简单的规则下想要赢得这个游戏并不那么容易。
游戏是这样的,在你面前有一圈整数(一共 $n$ 个),你要按顺序将其分为 $m$ 个部分,各部分内的数字相加,相加所得的 $m$ 个结果对 $10$ 取模后再相乘,最终得到一个数 $k$。
游戏的要求是使你所得的 $k$ 最大或者最小。 
例如,对于下面这圈数字($n=4,m=2$):
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202309271339712.png)
当要求 **最小值** 时,$((2−1)\ mod \ 10)×((4+3) \ mod\ 10)=1×7=7$
当要求 **最大值** 时,$((2+4+3)\ mod\ 10)×(−1\ mod\ 10)=9×9=81$
特别值得注意的是,无论是负数还是正数,对 $10$ 取模的结果均为非负值。
丁丁请你编写程序帮他赢得这个游戏。
**输入格式**
输入文件第一行有两个整数,$n$ 和 $m$。
以下 $n$ 行每行一个整数,其绝对值不大于 $10000$,按顺序给出圈中的数字,首尾相接。
**输出格式**
输出文件有两行,各包含一个非负整数。
第一行是你程序得到的最小值,第二行是最大值。
**数据范围**
$1≤n≤50,1≤m≤9$
**输入样例**:
```cpp {.line-numbers}
4 2
4
3
-1
2
```
**输出样例**:
```cpp {.line-numbers}
7
81
```
### 二、题目解析
($DP$,区间$DP$) $O(n^3m)$
首先 **将环从起点处断开,然后复制一遍接在后面**,这样原问题就转化成了线段型的模型。
> **注:破环成链**
```cpp {.line-numbers}
for (int i = 1; i <= n; i++) {
cin >> w[i];
w[i + n] = w[i]; // 破环成链
}
```
**状态表示**
$f[l][r][k]$:所有将区间$[l,r]$划分成$k$部分方案的乘积 **最小值**
$g[l][r][k]$:所有将区间$[l,r]$划分成$k$部分方案的乘积 **最大值**
**状态计算**
考虑$f[l][r][k]$如何计算获得,一般选取 **最后一步的操作**, **按最后一部分的位置来将$f[l][r][k]$所表示的所有方案划分成若干类**:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202309271449110.png)
> <font color='red' size=4><b>注:上面的是数字的点,而不是区间,不要混淆!可能会造成理解错误~【联想植树问题】</b></font>
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202309271506695.png)
> <font color='red' size=4><b>注:第$k$个部分,肯定是从$k-1$个部分转移而来,现在考虑的是划分完$k-1$部分后,最后一个点的所有可能性$[l+k-2,r-1]$</b></font>
设原$k-1$部分的终点是$j$,所以
$$\large l+k-2<=j<=r-1$$
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202309271505616.png)
思考增加了第$k$个部分,最大值、最小值怎么求呢?
前$k-1$个部分的终点是$j$,第$k$个部分的起点是从$[j+1,r]$的任意一个值。
就是
$$\large f[l][r][k]= f[l][j][k-1] * (sum(r)-sum(j+1)) \\
\large g[l][r][k]= g[l][j][k-1] * (sum(r)-sum(j+1))$$
>**解释**:
因为对于每个方案而言,它的得分就是前面的数乘上后面的数。
由于每个数都是非负的,如果我希望我最终的结果是最大值或最小值,而且,现在后面黄色方框中的数字大小是固定的,所以,前面最大就是整体最大,前面最小就是整体最小。
最终枚举所有长度是$n$的区间,取最大值/最小值即可。
$\large f[i][i+n][m] \\
\large g[i][i+n][m]$
**时间复杂度**
状态总共有 $O(n^2m)$ 个,计算每个状态需要 $O(n)$ 的计算量,因此总时间复杂度是 $O(n^3m)$。
### 三、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 10, INF = 0x3f3f3f3f;
int n, m;
int w[N], s[N];
int f[N][N][M], g[N][N][M];
int get_mod(int x) {
return (x % 10 + 10) % 10;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> w[i];
w[i + n] = w[i]; // 破环成链
}
for (int i = 1; i <= n * 2; i++) s[i] = s[i - 1] + w[i]; // 预处理前缀和,下标从1开始
memset(f, 0x3f, sizeof f); // 预求最小,先设最大
memset(g, -0x3f, sizeof g); // 预求最大,先设最小
for (int len = 1; len <= n; len++) // 区间DP,先枚举长度
for (int l = 1; l + len - 1 <= n * 2; l++) { // 枚举起点
int r = l + len - 1; // 计算终点
// DP 初始化
// 边界条件是全部划分为k=1,也就是一块时的结果是多少,根据题意,一整块当然就是区间内容相加后对10取模
f[l][r][1] = g[l][r][1] = get_mod(s[r] - s[l - 1]);
// 状态转移
for (int k = 2; k <= m; k++) // 枚举每个可以划分成的部分
for (int j = l + k - 2; j <= r - 1; j++) { // 枚举K-1部分时的最后一个终点位置
f[l][r][k] = min(f[l][r][k], f[l][j][k - 1] * get_mod(s[r] - s[j]));
g[l][r][k] = max(g[l][r][k], g[l][j][k - 1] * get_mod(s[r] - s[j]));
}
}
// 枚举每个区间长度为n的区间,获取符合长度要求的区间内,划分成m块的最大、最小值
int mx = -INF, mi = INF;
for (int i = 1; i <= n; i++) {
mx = max(mx, g[i][i + n - 1][m]);
mi = min(mi, f[i][i + n - 1][m]);
}
cout << mi << endl;
cout << mx << endl;
return 0;
}
```

View File

@@ -0,0 +1,22 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int m, n;
int f[N];
// 经典01背包
// 采药总时间相当于背包容量,每一株药相当于一件物品,采药时间相当于该物品的体积,草药的价值相当于物品价值。
int main() {
cin >> m >> n;
for (int i = 0; i < n; i++) {
int v, w; // v体积,w价值
cin >> v >> w;
for (int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m] << endl;
return 0;
}

View File

@@ -0,0 +1,31 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 30010;
int n, m;
int f[N];
/*
将原问题做如下转化
总钱数相当于背包总容量
每件物品的价格相当于体积
每件物品的价格乘以重要度相当于价值
那么就变成了经典的01背包问题。
*/
int main() {
cin >> m >> n;
for (int i = 0; i < n; i++) {
int v, w;
cin >> v >> w;
w *= v;
for (int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m] << endl;
return 0;
}

View File

@@ -0,0 +1,197 @@
## [$AcWing$ $431$. 守望者的逃离](https://www.acwing.com/problem/content/description/433/)
### 一、题目描述
恶魔猎手尤迪安野心勃勃,他背叛了暗夜精灵,率领深藏在海底的娜迦族企图叛变。
守望者在与尤迪安的交锋中遭遇了围杀,被困在一个荒芜的大岛上。
为了杀死守望者,尤迪安开始对这个荒岛施咒,这座岛很快就会沉下去。
到那时,岛上的所有人都会遇难。
守望者的 **跑步速度** 为 $17m/s$,以这样的速度是无法逃离荒岛的。
庆幸的是守望者拥有 **闪烁法术**,可在 $1s$ 内移动 $60m$,不过每次使用闪烁法术都会 **消耗魔法值 $10$点**。
守望者的魔法值恢复的速度为 $4$ 点/$s$,只有 **处在原地休息状态时才能恢复**。
现在已知守望者的魔法初值 $M$,他所在的初始位置与岛的出口之间的距离 $S$,岛沉没的时间 $T$。
你的任务是写一个程序帮助守望者计算如何在 **最短的时间内** 逃离荒岛,若不能逃出,则输出守望
者在剩下的时间能走的最远距离。
**注意**:守望者跑步、闪烁或休息活动均以秒($s$)为单位,且每次活动的持续时间为整数秒。距离的单位为米($m$)。
**输入格式**
输入文件仅一行,包括空格隔开的三个非负整数 $M,S,T$。
**输出格式**
输出文件包括两行:
第 $1$ 行为字符串 `Yes` 或 `No`(区分大小写),即守望者是否能逃离荒岛。
第 $2$ 行包含一个整数,第一行为 `Yes` 时表示守望者逃离荒岛的最短时间;第一行为 `No` 时表示守望者能走的最远距离。
**数据范围**
$1≤T≤300000,0≤M≤1000,1≤S≤10^8$
**输入样例**:
```cpp {.line-numbers}
39 200 4
```
**输出样例**:
```cpp {.line-numbers}
No
197
```
**输入样例#2**:
```cpp {.line-numbers}
36 255 10
```
**输出样例#2**:
```cpp {.line-numbers}
Yes
6
```
### 二、解题思路
机房$dalao$:一看就是弱智$dp$题。然后他切了。但是我因为太菜第一想法是全都进行闪现。下面进行分析。
每次恢复魔力值$10$点需要$2.5s$,再用$1s$进行$60m$的闪现,$60/3.5$大概是$17.14m/s$的速度,刚出发的时候还有一些魔力值。跑步速度是$17m/s$,这样看来似乎闪现是最优选,我们愉快的贪心吧!!于是我立马写了个代码交上:
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
int m; // 初始魔法值
int s; // 他所在的初始位置与岛的出口之间的距离
int t; // 岛沉没的时间
int d; // 守望者在剩下的时间内能走的最远距离
// 贪心思路,可能过 5/11
int main() {
cin >> m >> s >> t;
for (int i = 1; i <= t; i++) { // 枚举每一秒
if (m >= 10) { // 如果剩余魔法值大于10,那么可以进行闪烁
m -= 10; // 魔法值减10
d += 60; // 距离增加60
} else
m += 4; // 停下来,消耗1秒,魔法值恢复4点
if (d >= s) { // 如果走的距离已经大于目标距离,可以走出
cout << "Yes" << endl; // 说明可以走出去
cout << i << endl; // 输出需要几秒
return 0;
}
}
cout << "No" << endl; // 走不出去
cout << d << endl; // 最远可以走到d这个位置上
return 0;
}
```
很好,样例都没过。但是我怎么可能这么轻易认输呢,我干脆点了提交。
![](https://img2018.cnblogs.com/blog/1665384/201905/1665384-20190516095604803-1855155708.png)
虽然$WA$了一半,但$A$了一半说明它并不是全无道理。或许可以推出正确的思路呢?
以样例为例,`39 200 4`这组数据正确的最远距离结果是$197$,这份代码输出的结果则是$180$.我们可以看出来,最后一秒中本可以跑步$17m$,这份代码却会让守望者原地等待。
再看`36 255 10`这组,正确的最短时间是$6s$,这份程序的结果是$9s$。我们人工模拟一下,可以发现在$5s-6s$时我们可以选择跑步啊!而这个废物代码却会原地等待$3s$后再进行闪现。
这时我又觉得我会了!只要判断一下不就好了吗!
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
int m; // 初始魔法值
int s; // 他所在的初始位置与岛的出口之间的距离
int t; // 岛沉没的时间
int d; // 守望者在剩下的时间内能走的最远距离
int main() {
cin >> m >> s >> t;
for (int i = 1; i <= t; i++) { // 枚举每一秒
if (s - d <= 17) { // 如果距离小于17,那么可以在1秒内跑步走完,不用再等魔法值恢复
cout << "Yes" << endl; // 说明可以走出去
cout << i << endl; // 输出需要几秒
return 0;
}
if (i == t && m < 10) { // 时间到达最后1秒,还剩下1秒的时间,并且不足以进行一次闪烁
cout << "No" << endl;
cout << d + 17 << endl; // 这1秒跑步吧
return 0;
}
if (m >= 10) { // 能闪烁尽量闪烁
m -= 10; // 用一次闪烁,魔法值就减少10个
d += 60; // 能跑60米
} else
m += 4; // 本秒用于回血,1秒可以恢复4个血
if (d >= s) {
cout << "Yes" << endl; // 说明可以走出去
cout << i << endl; // 输出需要几秒
return 0;
}
}
cout << "No" << endl; // 走不出去
cout << d << endl; // 最远可以走到d这个位置上
return 0;
}
```
好,这次看起来好多了,我们再提交一下试试。
![](https://img2018.cnblogs.com/blog/1665384/201905/1665384-20190516100418666-588346398.png)
只是多$A$了一个点,说明优化还是有问题。思考过后我们可以想到,每秒中我们有三种决定:**闪现、跑步和停留**。停留和闪现可以合并在一起,**我们的优化只给了最后一秒跑步的选择**。
于是我们可以运用一种似乎很像动态规划的解法啦!我们先假设全部进行闪现-停留-闪现操作,再加一个循环判断每秒的最优决策究竟是闪现-停留还是跑步。我们用$f[i]$数组表示第$i$秒时守望者 **所能到的最远距离**。
```cpp {.line-numbers}
#include <iostream>
using namespace std;
const int N = 300010;
int m; // 初始魔法值
int s; // 他所在的初始位置与岛的出口之间的距离
int t; // 岛沉没的时间
int f[N]; // f[i]数组表示第i秒时守望者所能到的最远距离
int main() {
cin >> m >> s >> t;
for (int i = 1; i <= t; i++) { // 相当于第一次提交的程序
if (m >= 10) { // 能闪烁尽量闪烁
m -= 10;
f[i] = f[i - 1] + 60; // 最大距离可以增加60米
} else {
m += 4; // 不能闪烁,那么就站在原地回血
f[i] = f[i - 1]; // 距离没有长大
}
}
// 问题是站在原地回血不一定是最优选择,也可以在此时选择跑步啊~
for (int i = 1; i <= t; i++) {
f[i] = max(f[i], f[i - 1] + 17); // 如果在第i秒时跑步能到达更远距离,我们跑步
if (f[i] >= s) { // 已到达就可以输出+结束程序啦
cout << "Yes" << endl;
cout << i << endl;
return 0;
}
}
cout << "No" << endl;
cout << f[t] << endl; // 如果进行到了这里说明无法逃离,我们输出能到达的最远距离
return 0;
}
```
这次的样例也过了,于是我们提交一下。
![](https://img2018.cnblogs.com/blog/1665384/201905/1665384-20190516101056706-1730888110.png)
一道黄题耗我十五分钟我果然还是太菜了
这样就通过了!如果有兴趣的话大家可以研究一下暴力写法,也是能过的(我懒得想了
谨记教训:犹豫就会败北,果断就会白给!!!

View File

@@ -0,0 +1,32 @@
#include <iostream>
using namespace std;
const int N = 300010;
int m; // 初始魔法值
int s; // 他所在的初始位置与岛的出口之间的距离
int t; // 岛沉没的时间
int f[N]; // f[i]数组表示第i秒时守望者所能到的最远距离
int main() {
cin >> m >> s >> t;
for (int i = 1; i <= t; i++) { // 相当于第一次提交的程序
if (m >= 10) { // 能闪烁尽量闪烁
m -= 10;
f[i] = f[i - 1] + 60; // 最大距离可以增加60米
} else {
m += 4; // 不能闪烁,那么就站在原地回血
f[i] = f[i - 1]; // 距离没有长大
}
}
// 问题是站在原地回血不一定是最优选择,也可以在此时选择跑步啊~
for (int i = 1; i <= t; i++) {
f[i] = max(f[i], f[i - 1] + 17); // 如果在第i秒时跑步能到达更远距离,我们跑步
if (f[i] >= s) { // 已到达就可以输出+结束程序啦
cout << "Yes" << endl;
cout << i << endl;
return 0;
}
}
cout << "No" << endl;
cout << f[t] << endl; // 如果进行到了这里说明无法逃离,我们输出能到达的最远距离
return 0;
}

View File

@@ -0,0 +1,27 @@
#include <bits/stdc++.h>
using namespace std;
int m; // 初始魔法值
int s; // 他所在的初始位置与岛的出口之间的距离
int t; // 岛沉没的时间
int d; // 守望者在剩下的时间内能走的最远距离
// 贪心思路,可能过 5/11
int main() {https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/extracted/26c367a3c3cf20e62e1d6c7904f6abf6/9DFECB5E-FBA7-4a26-B6BC-A31BF4B184CA.png
cin >> m >> s >> t;
for (int i = 1; i <= t; i++) { // 枚举每一秒
if (m >= 10) { // 如果剩余魔法值大于10,那么可以进行闪烁
m -= 10; // 魔法值减10
d += 60; // 距离增加60
} else
m += 4; // 停下来,消耗1秒,魔法值恢复4点
if (d >= s) { // 如果走的距离已经大于目标距离,可以走出
cout << "Yes" << endl; // 说明可以走出去
cout << i << endl; // 输出需要几秒
return 0;
}
}
cout << "No" << endl; // 走不出去
cout << d << endl; // 最远可以走到d这个位置上
return 0;
}

View File

@@ -0,0 +1,39 @@
#include <bits/stdc++.h>
using namespace std;
int m; // 初始魔法值
int s; // 他所在的初始位置与岛的出口之间的距离
int t; // 岛沉没的时间
int d; // 守望者在剩下的时间内能走的最远距离
int main() {
cin >> m >> s >> t;
for (int i = 1; i <= t; i++) { // 枚举每一秒
if (s - d <= 17) { // 如果距离小于17,那么可以在1秒内跑步走完,不用再等魔法值恢复
cout << "Yes" << endl; // 说明可以走出去
cout << i << endl; // 输出需要几秒
return 0;
}
if (i == t && m < 10) { // 时间到达最后1秒,还剩下1秒的时间,并且不足以进行一次闪烁
cout << "No" << endl;
cout << d + 17 << endl; // 这1秒跑步吧
return 0;
}
if (m >= 10) { // 能闪烁尽量闪烁
m -= 10; // 用一次闪烁,魔法值就减少10个
d += 60; // 能跑60米
} else
m += 4; // 本秒用于回血,1秒可以恢复4个血
if (d >= s) {
cout << "Yes" << endl; // 说明可以走出去
cout << i << endl; // 输出需要几秒
return 0;
}
}
cout << "No" << endl; // 走不出去
cout << d << endl; // 最远可以走到d这个位置上
return 0;
}

View File

@@ -0,0 +1,26 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;
int f[N][N];
int main() {
cin >> n >> m;
// 边界:第0次传球,第1个人获得球的机会数
f[0][1] = 1;
// 传m次球
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (j == 1)
f[i][j] = f[i - 1][j + 1] + f[i - 1][n];
else if (j == n)
f[i][j] = f[i - 1][j - 1] + f[i - 1][1];
else
f[i][j] = f[i - 1][j - 1] + f[i - 1][j + 1];
}
}
// 经过m次传球,球回到第1个人手中的机会数
cout << f[m][1] << endl;
return 0;
}

View File

@@ -0,0 +1,82 @@
## [$AcWing$ $435$. 传球游戏](https://www.acwing.com/problem/content/437/)
### 一、题目描述
上体育课的时候,小蛮的老师经常带着同学们一起做游戏。
这次,老师带着同学们一起做传球游戏。
游戏规则是这样的:$n$ 个同学站成一个圆圈,其中的一个同学手里拿着一个球,当老师吹哨子时开始传球,每个同学可以把球传给自己左右的两个同学中的一个(左右任意),当老师再次吹哨子时,传球停止,此时,拿着球没传出去的那个同学就是败者,要给大家表演一个节目。
聪明的小蛮提出一个有趣的问题:有多少种不同的传球方法可以使得从小蛮手里开始传的球,传了 $m$ 次以后,又回到小蛮手里。
两种传球的方法被视作不同的方法,当且仅当这两种方法中,接到球的同学按接球顺序组成的序列是不同的。
比如有 $3$ 个同学 $1$ 号、$2$ 号、$3$ 号,并假设小蛮为 $1$ 号,球传了 $3$ 次回到小蛮手里的方式有 $1→2→3→1$ 和 $1→3→2→1$,共 $2$ 种。
**输入格式**
输入文件共一行,有两个用空格隔开的整数 $n,m$。
**输出格式**
输出文件共一行,有一个整数,表示符合题意的方法数。
**数据范围**
$3≤n≤30,1≤m≤30$
**输入样例**:
```cpp {.line-numbers}
3 3
```
**输出样例**:
```cpp {.line-numbers}
2
```
### 二、解题思路
**($DP$,环形$DP$) $O(NM)$**
不妨设小蛮在$0$号,所有人的编号是 $0 \sim n−1$。
#### 状态表示 $f[i, j]$:
- **集合**:所有已经传了$i$次球,且最后球在编号是$j$的小朋友手上的方案
- **属性**:集合中元素的数量;
#### 状态计算:
我们可以发现,任何一个位置都只能从左边和右边传过来,这样我们就可以列出我们的方程:
① $\large f[i][j] = f[i-1][2] + f[i-1][n] \ \ \ \ \ \ (j=1)$
② $\large f[i][j] = f[i-1][1] + f[i-1][n-1] (j=n)$
③ $\large f[i][j] = f[i-1][j-1] + f[i-1][j+1] (j \neq 1,j \neq n) $
边界:
$\large f[0][1] = 1$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;
int f[N][N];
int main() {
cin >> n >> m;
// 边界:第0次传球,第1个人获得球的机会数
f[0][1] = 1;
// 传m次球
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (j == 1)
f[i][j] = f[i - 1][j + 1] + f[i - 1][n];
else if (j == n)
f[i][j] = f[i - 1][j - 1] + f[i - 1][1];
else
f[i][j] = f[i - 1][j - 1] + f[i - 1][j + 1];
}
}
// 经过m次传球,球回到第1个人手中的机会数
cout << f[m][1] << endl;
return 0;
}
```

View File

@@ -0,0 +1,25 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 110, mod = 1000007;
int n, m;
int f[N];
int main() {
cin >> n >> m;
f[0] = 1;
for (int i = 1; i <= n; i++) { // 从1到n种花
int a;
cin >> a; // 最多a盆
for (int j = m; j >= 0; j--) // 花m盆花
for (int k = 1; k <= min(a, j); k++)
f[j] = (f[j] + f[j - k]) % mod;
}
cout << f[m] << endl;
return 0;
}

View File

@@ -0,0 +1,32 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 110, mod = 1000007;
int n, m;
int f[N][N]; // f[i][j]: 使用前i种花,摆出j盆花的方案数
int main() {
cin >> n >> m;
// 初始化:没有花时,摆0盆花有1种方案
f[0][0] = 1; // base 边界
for (int i = 1; i <= n; i++) { // 枚举花种
int a;
cin >> a; // 当前花最多取a盆
// 枚举摆放总数
for (int j = 0; j <= m; j++) {
// 枚举当前花种取k盆
for (int k = 0; k <= min(a, j); k++) {
// 状态转移:当前状态 += 前i-1种花摆j-k盆的方案数
f[i][j] = (f[i][j] + f[i - 1][j - k]) % mod;
}
}
}
cout << f[n][m] << endl;
return 0;
}

View File

@@ -0,0 +1,41 @@
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL INF = 1e17;
const int N = 1e6 + 10;
int n, p;
LL a[N]; // ÿ<><C3BF>С<EFBFBD><D0A1><EFBFBD>ѵ<EFBFBD><D1B5><EFBFBD><EFBFBD><EFBFBD>(<28><><EFBFBD><EFBFBD>)
LL b[N]; // ÿ<><C3BF>С<EFBFBD><D0A1><EFBFBD>ѵ<EFBFBD><D1B5><EFBFBD><EFBFBD><EFBFBD>(֮ǰ<D6AE><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӷκ<D3B6>)
LL c[N]; // ÿ<><C3BF>С<EFBFBD><D0A1><EFBFBD>ѵķ<D1B5><C4B7><EFBFBD>(c[1]=b[1]<5D><>c[i]=c[k]+b[k])
int main() {
// <20>ӿ<EFBFBD><D3BF><EFBFBD><EFBFBD><EFBFBD>
ios::sync_with_stdio(false), cin.tie(0);
// <20><><EFBFBD><EFBFBD>
cin >> n >> p;
for (int i = 1; i <= n; i++)
cin >> a[i];
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӷκ<D3B6>
b[0] = -INF;
LL sum = 0;
for (int i = 1; i <= n; i++) {
sum += a[i];
b[i] = max(b[i - 1], sum);
sum = max(sum, (LL)0);
}
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
c[1] = b[1];
c[2] = c[1] + b[1];
for (int i = 3; i <= n; i++) {
c[i] = max(c[i - 1], c[i - 1] + b[i - 1]);
if (c[i] > c[1]) // ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
c[i] %= p;
}
if (c[1] > 0) // <20><><EFBFBD><EFBFBD><EFBFBD>Dz<EFBFBD><C7B2>½<EFBFBD><C2BD><EFBFBD>,<2C><>c[1]>0ʱ<30><CAB1>c[n]>c[1]
cout << c[n] % p;
else
cout << max(c[n], c[1]) % p;
return 0;
}

View File

@@ -0,0 +1,72 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
const int INF = 0x3f3f3f3f;
int n, m, r, c; // n行m列的矩阵,从中选择r行c列
int a[N][N]; // 原始矩阵
int f[N][N]; // DP数组
int cw[N]; // 每一列内部的代价,column代价,用cw表示(在行确定的情况下)
int rw[N][N];
// 任意两列之间的代价,因为列之间可能不连续,所以需要用二维描述
// 比如rw[3][6],描述第3列与第6列被选中,它们之间相邻,计算它们之间的差值绝对值,
// 也就是横向代价。
int q[N];
// 计算一个二进制数中有多少个数字1
int count(int x) {
int s = 0;
for (int i = 0; i < n; i++) s += x >> i & 1;
return s;
}
int main() {
cin >> n >> m >> r >> c;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> a[i][j];
int res = INF;
for (int st = 0; st < 1 << n; st++) // 二进制枚举
if (count(st) == r) { // 如果二进制的状态表示中,数字1的个数与目标值相同
for (int i = 0, j = 0; i < n; i++)
if (st >> i & 1) q[j++] = i; // 记录选择了哪几行
// 预处理出每一列(i列)(在选择r行后,形成的小矩阵情况下)
// 数字上下之间的abs(差值)
for (int i = 0; i < m; i++) { // 枚举每一列
cw[i] = 0; // 计算每一列数字上下之间的abs(差值)和
for (int j = 1; j < r; j++) // 选择的每一行
cw[i] += abs(a[q[j]][i] - a[q[j - 1]][i]);
}
// 如果i与j列选择后相邻,预处理出sum(abs(差值)),方便DP增量时使用
// j一定要在i后面才有意义
for (int i = 0; i < m; i++) // 枚举每一列,开始列
for (int j = i + 1; j < m; j++) { // 从i列到j列,结束列
rw[i][j] = 0; // 多轮DP,需要手动初始化后才能进行处理
for (int k = 0; k < r; k++) // 选择的每一行
rw[i][j] += abs(a[q[k]][i] - a[q[k]][j]);
// 将多行的相邻(i,j)列之间的所有abs(差值)都汇总到rw[i][j]
}
// DP
for (int i = 0; i < m; i++) { // 枚举每一列
f[i][1] = cw[i]; // 第i个数结尾,长度为1的子矩阵,没有横向的,只有纵向的
for (int j = 2; j <= c; j++) {
f[i][j] = INF;
// 向前寻找前一个有效位置k
for (int k = 0; k < i; k++)
// 两者间的转移关系= + 纵向i列转移代价 + (k,i)之间的横向转移代价
f[i][j] = min(f[i][j], f[k][j - 1] + cw[i] + rw[k][i]);
}
// 每次的结果都有机会参加评比
res = min(res, f[i][c]);
}
}
// 输出最终的最小值
cout << res << endl;
return 0;
}

View File

@@ -0,0 +1,179 @@
## [$AcWing$ $460$ 子矩阵](https://www.acwing.com/problem/content/462/)
### 一、题目描述
给出如下定义:
子矩阵:从一个矩阵当中选取某些行和某些列 **交叉位置** 所组成的新矩阵(保持行与列的相对顺序)被称为原矩阵的一个子矩阵。
例如,下面左图中选取第 $2、4$ 行和第 $2、4、5$ 列交叉位置的元素得到一个 $2×3$ 的子矩阵如右图所示。
![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/10/212a1b14bd6e771e4ac1fe3b1211bfb2.png)
**相邻的元素**:矩阵中的某个元素与其上下左右四个元素(如果存在的话)是相邻的。
**矩阵的分值**:矩阵中每一对相邻元素之差的绝对值之和。
本题任务:给定一个 $n$ 行 $m$ 列的正整数矩阵,请你从这个矩阵中选出一个 $r$ 行 $c$ 列的子矩阵,使得这个子矩阵的分值最小,并输出这个分值。
**输入格式**
第一行包含用空格隔开的四个整数 $n,m,r,c$,意义如问题描述中所述,每两个整数之间用一个空格隔开。
接下来的 $n$ 行,每行包含 $m$ 个用空格隔开的整数(均不超过 $1000$),用来表示问题描述中那个 $n$行 $m$ 列的矩阵。
**输出格式**
输出共 $1$ 行,包含 $1$ 个整数,表示满足题目描述的子矩阵的最小分值。
**数据范围**
$1≤n,m≤16,1≤r≤n,1≤c≤m$
**输入样例 1**:
```cpp {.line-numbers}
5 5 2 3
9 3 3 3 9
9 4 8 7 4
1 7 4 6 6
6 8 5 6 9
7 4 5 6 1
```
**输出样例 1**:
```cpp {.line-numbers}
6
```
> **解析**:
> 样例$1$:
该矩阵中分值最小的 $2$ 行 $3$ 列的子矩阵由原矩阵的第 $4$ 行、第 $5$ 行与第 $1$ 列、第 $3$ 列、第 $4$ 列交叉位置的元素组成,为
```cpp {.line-numbers}
6 5 6
7 5 6
```
> 其分值为: $|6-5| + |5-6| + |7-5| + |5-6| + |6-7| + |5-5| + |6-6| =6$。
**输入样例 2**:
```cpp {.line-numbers}
7 7 3 3
7 7 7 6 2 10 5
5 8 8 2 1 6 2
2 9 5 5 6 1 7
7 9 3 6 1 7 8
1 9 1 4 7 8 8
10 5 9 1 1 8 10
1 3 1 5 4 8 6
```
**输出样例 2**:
```cpp {.line-numbers}
16
```
> **解析**
> 样例$2$:
该矩阵中分值最小的$3$行$3$列的子矩阵由原矩阵的第 $4$ 行、第 $5$ 行、第 $6$ 行与第 $2$ 列、第 $6$ 列、第 $7$ 列交叉位置的元素组成,选取的分值最小的子矩阵为
```cpp {.line-numbers}
9 7 8
9 8 8
5 8 10
```
### 二、题目解析
(枚举,$DP$,线性$DP$)
$O(C_n^rn^3)$
当选定的行固定时,问题变成:
> **给定一个长度为 $m$ 的序列,从中选出一个长度为 $c$ 的子序列。序列中的每个元素均有一个分值,且任意相邻两个被选出的元素,也会产生一个分值。问:如何选择子序列可使分值之和最小**。
这是一个经典的序列$DP$模型:
**状态表示**
$f[i][j]$表示所有以第$i$个数结尾,且长度为$j$的子序列的分值之和的最小值。
**状态计算**
以倒数第二个数是哪个数为依据,将$f[i][j]$所代表的集合分成若干类,则倒数第二个数是第$k$个数的所有子序列的最小分值是
$$f[k][j - 1] + cost()$$
其中$cost()$是在序列末尾加上第$i$个数所产生的分值。
$f[i][j]$取所有类别的最小分值即可。
由于 $n$ 较小,我们可以直接枚举行的所有选择,然后用上述做法$DP$即可。
**时间复杂度**
一共有 $n$ 行,从中选出 $r$ 行,总共有 $C_n^r$ 种选择,对于每种选择,$DP$的状态总共有 $O(n^2)$个,计算每个状态需要 $O(n)$ 的计算量,因此$DP$的时间复杂度是 $O(n^3)$。所以总时间复杂度是 $O(C_n^rn^3)$。
### 三、实现代码
$Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
const int INF = 0x3f3f3f3f;
int n, m, r, c; // n行m列的矩阵,从中选择r行c列
int a[N][N]; // 原始矩阵
int f[N][N]; // DP数组
int cw[N]; // 每一列内部的代价,column代价,用cw表示(在行确定的情况下)
int rw[N][N];
// 任意两列之间的代价,因为列之间可能不连续,所以需要用二维描述
// 比如rw[3][6],描述第3列与第6列被选中,它们之间相邻,计算它们之间的差值绝对值,
// 也就是横向代价。
int q[N];
// 计算一个二进制数中有多少个数字1
int count(int x) {
int s = 0;
for (int i = 0; i < n; i++) s += x >> i & 1;
return s;
}
int main() {
cin >> n >> m >> r >> c;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> a[i][j];
int res = INF;
for (int st = 0; st < 1 << n; st++) // 二进制枚举
if (count(st) == r) { // 如果二进制的状态表示中,数字1的个数与目标值相同
for (int i = 0, j = 0; i < n; i++)
if (st >> i & 1) q[j++] = i; // 记录选择了哪几行
// 预处理出每一列(i列)(在选择r行后,形成的小矩阵情况下)
// 数字上下之间的abs(差值)
for (int i = 0; i < m; i++) { // 枚举每一列
cw[i] = 0; // 计算每一列数字上下之间的abs(差值)和
for (int j = 1; j < r; j++) // 选择的每一行
cw[i] += abs(a[q[j]][i] - a[q[j - 1]][i]);
}
// 如果i与j列选择后相邻,预处理出sum(abs(差值)),方便DP增量时使用
// j一定要在i后面才有意义
for (int i = 0; i < m; i++) // 枚举每一列,开始列
for (int j = i + 1; j < m; j++) { // 从i列到j列,结束列
rw[i][j] = 0; // 多轮DP,需要手动初始化后才能进行处理
for (int k = 0; k < r; k++) // 选择的每一行
rw[i][j] += abs(a[q[k]][i] - a[q[k]][j]);
// 将多行的相邻(i,j)列之间的所有abs(差值)都汇总到rw[i][j]
}
// DP
for (int i = 0; i < m; i++) { // 枚举每一列
f[i][1] = cw[i]; // 第i个数结尾,长度为1的子矩阵,没有横向的,只有纵向的
for (int j = 2; j <= c; j++) {
f[i][j] = INF;
// 向前寻找前一个有效位置k
for (int k = 0; k < i; k++)
// 两者间的转移关系= + 纵向i列转移代价 + (k,i)之间的横向转移代价
f[i][j] = min(f[i][j], f[k][j - 1] + cw[i] + rw[k][i]);
}
// 每次的结果都有机会参加评比
res = min(res, f[i][c]);
}
}
// 输出最终的最小值
cout << res << endl;
return 0;
}
```

View File

@@ -0,0 +1,180 @@
## [$AcWing$ $472$. 跳房子](https://www.acwing.com/problem/content/474/)
### 一、题目描述
跳房子,也叫跳飞机,是一种世界性的儿童游戏,也是中国民间传统的体育游戏之一。
跳房子的游戏规则如下: 
在地面上确定一个起点,然后在起点右侧画 $n$ 个格子,这些格子都在同一条直线上。
每个格子内有一个数字(整数),**表示到达这个格子能得到的分数**。
玩家第一次从起点开始向右跳,跳到起点右侧的一个格子内。
第二次再从当前位置继续向右跳,依此类推。
规则规定:玩家每次都必须跳到当前位置右侧的一个格子内。
玩家可以在 **任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和**。 
现在小 $R$ 研发了一款弹跳机器人来参加这个游戏。
但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的 $d$。
小 $R$ 希望改进他的机器人,如果他花 $g$ 个金币改进他的机器人,那么他的机器人灵活性就能增加 $g$,但是需要注意的是,每次弹跳的距离至少为 $1$。
具体而言,当 $g<d$ 时,他的机器人每次可以选择向右弹跳的距离为 $d−g, d−g+1, d−g+2,…,d+g−2,d+g−1,d+g$;否则(当 $g≥d$ 时),他的机器人每次可以选择向右弹跳的距离为 $1,2,3,…,d+g−2,d+g−1,d+g$。 
现在小 $R$ 希望获得 **至少** $k$ 分,请问他 **至少要花多少金币** 来改造他的机器人。
**输入格式**
第一行三个正整数 $n,d,k$,分别表示格子的数目,改进前机器人弹跳的固定距离,以及希望至少获得的分数,相邻两个数之间用一个空格隔开。
接下来 $n$ 行,每行两个正整数 $x_i,s_i$,分别表示起点到第 $i$ 个格子的距离以及第 $i$ 个格子的分数。
两个数之间用一个空格隔开,保证 $x_i$ 按递增顺序输入。
**输出格式**
共一行,一个整数,表示至少要花多少金币来改造他的机器人。
若无论如何他都无法获得至少 $k$ 分,输出 $−1$。
**数据范围**
$1≤n≤500000,1≤d≤2000,1≤x_i,k≤10^9,|s_i|≤10^5$
**输入样例**:
```cpp {.line-numbers}
7 4 10
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2
```
**输出样例**:
```cpp {.line-numbers}
2
```
**样例解读**
$A$-$G$分别表示$1$号到$7$号格子,红色文字表示该格子对应的分值。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310081607864.png)
初始状态,当$d = 4$时,无法由起点跳到其它点,此时需要花费$2$金币改造,改造后机器人的移动范围变为$[2,6]$,此时:
1. 机器人跳到$A$点,得$6$分,总分$6$分
2. 机器人跳到$B$点,得$-3$分,总分$3$分
3. 机器人跳到$C$点,得$3$分,总分$6$分
4. 机器人跳到$E$点,得$1$分,总分$7$分
5. 机器人跳到$F$点,得$6$分,总分$13$分
所以,当花费$2$金币进行改造时,得分不低于$10$分。
### 二、题目解析
#### 1、想到二分
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310091459366.png)
假设答案为$ans$,那么必然存在一种跳法,使得可以得到$>=k$的分数,此时的跳跃步数为$[d-ans,d+ans]$。
此时,如果增大$ans$,那么上面的跳跃区间就会因为$ans$的变大而区间严格变大。
所以,本题可以通过二分来以$log_2n$的复杂度来去掉一维,就是评估一个$g$值,问它是不是可以满足要求。
#### 2、动态规划 + 单调队列优化
假设$f[i]$表示到达$i$号格子时可以获得的最大分值,来思考它是从哪个格子转移过来的:(**动态规划**)
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/2023-10-08_211034.png)
在给定 $g$个金币的情况下,机器人可以跳$d$这么远,如果现在在$x$个位置上,那么机器人可以跳到距离范围就是
$$\large [d-g , d+g]$$
也就是说,距离$i$点距离在$(d-g),(d+g)$之间的所有格子都是可以跳到$i$这个格子上来了,这是一个固定长度的区间,我们需要在这个区间上找出所有的格子,看看哪个格子的$f[x]$最大,就选择从它这个位置上跳到$i$上去,这就是一个前序固定长度区间找最大值问题,想到用滑动窗口求最值问题,使用 **单调队列** 进行优化。
我们假设可以跳到$i$号格子的前序区间 **距离范围** 是$[j,k]$,这个范围内的所有点都可能是$f[i]$的前序转移点:
$$\large f[i]=f[y]+w[i]$$
那如何确定这个$y$是哪个点呢?就是这个区间里$f[]$最大值的那个嘛~
**思路与步骤**:
① $DP$+动态规划
② 从左到右去填充$f[i]$统计结果数据表,动态维护一个范围为$[d-g,d+g]$的范围,用$x[i]$与$x[y]$之间的距离差($x[i]>x[y]$)判断是不是在上面的范围内
### 三、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
typedef long long LL;
int n, d, k; // n:格子数,d:机器人可以跳的距离,k:想要取得的分值
int x[N], w[N]; // 距离数组x[i]表示i号格子距离出发点的距离,w[i]从i号格子中可以获得的分值
LL f[N]; // f[i]:到达i点时可以获取到的最大分值
int q[N]; // 单调队列,记录的是格子号
bool check(int g) { // g 描述的是给定的金币数量
LL res = 0;
// 多次dp,每次需要清空统计数组
memset(f, -0x3f, sizeof f); // 预求最大,先设最小
f[0] = 0; // 递推起点数据,分值为0,这个要先看状态转移方程,再思考整体初始化、起点初始化
int hh = 0, tt = -1;
// L,R : 左右边界
int L = max(1, d - g), R = d + g;
/*
④ 为什么要先处理入队列,再处理出队列?反过来为什么是错的呢?
答:
*/
for (int i = 1, j = 0, k = 0; i <= n; i++) {
// ① 新元素入队列
while (x[i] - x[k] >= L) { // i走的挺快,k有资格参评了
while (hh <= tt && f[q[tt]] <= f[k]) tt--; // 年龄比k大,值比k小的都去死
q[++tt] = k++; // k入队列
}
// ② 越界元素出队列
while (x[i] - x[j] > R) j++; // j出界,j无法跳到i
while (hh <= tt && q[hh] < j) hh++; // j都出界了,单调队列也需要维护,把比j小的都干掉
// ③ 队列非空,队列头中保存的就是前面[L,R]范围内的分数最大值所对应的y号格子
if (hh <= tt) f[i] = f[q[hh]] + w[i]; // 再加上i号格子中的w[i]分值,就是总分值
// 时刻更新最大值
res = max(res, f[i]);
}
// 因为分值中存在负数,一旦是在半途中出现可以获取到的分值大于等于k时,就可以停止掉,表示已经找到了一个金币数量满足增加了自由度后,可以取得k这样的分值
return res >= k;
}
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> d >> k; // 格子数量,机器人每次可以跳的距离,想要拿到的分数
for (int i = 1; i <= n; i++) cin >> x[i] >> w[i]; // 起点到第i个格子的距离,第i个格子的分数
int L = 0, R = 1e9, ans = -1; // 二分的左右无脑边界值
/*
二分的最后结果有两种方式:
1、使用ans变量记录最终结果,ans初始化为-1,每次找到check()通过的mid,ans=mid,这样,如果最终有答案ans就记录的是答案,否则就记录的是-1。
2、不使用ans变量记录,而是最后再用!check(L)然后输出-1。
个人认为方法1更易理解。
*/
while (L < R) {
int mid = L + R >> 1;
if (check(mid)) {
R = mid;
ans = mid;
} else
L = mid + 1;
}
printf("%d\n", ans);
return 0;
}
```

View File

@@ -0,0 +1,76 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
typedef long long LL;//五年OI一场空,不开LONG LONG
int n, d, k; // n:格子数,d:机器人可以跳的距离,k:想要取得的分值
int x[N], w[N]; // 距离数组x[i]表示i号格子距离出发点的距离,w[i]从i号格子中可以获得的分值
LL f[N]; // f[i]:到达i点时可以获取到的最大分值
int q[N]; // 单调队列,记录的是格子号
bool check(int g) { // g 描述的是给定的金币数量
LL res = 0;
// 多次dp,每次需要清空统计数组
memset(f, -0x3f, sizeof f); // 预求最大,先设最小
f[0] = 0; // 递推起点数据,分值为0,这个要先看状态转移方程,再思考整体初始化、起点初始化
int hh = 0, tt = -1;
// L,R : 左右边界
int L = max(1, d - g), R = d + g;
/*
④ 为什么要先处理入队列,再处理出队列?反过来为什么是错的呢?
答:我们向单调队列中添加节点的逻辑是发现x[i]-x[k]>=L,也就是k没有进过队列,没有参加
评比过,并且,两个格子间的距离大于了L,就让它进来试试。
但是,这样就真的可以了吗?因为我们知道,单调队列中保存是一个个格子号,而这些格子号必须在i左侧指定的范围内才行。
x[i]-x[k]>=L是成立了,那会不会k离i太远了,超过了上限R呢?如果是这样的话,那么k是不能停留在队列中的,需要去除掉。
*/
for (int i = 1, j = 0, k = 0; i <= n; i++) {
// ① 新元素入队列
while (x[i] - x[k] >= L) { // i走的挺快,k有资格参评了
while (hh <= tt && f[q[tt]] <= f[k]) tt--; // 年龄比k大,值比k小的都去死
q[++tt] = k++; // k入队列
}
// ② 越界元素出队列
while (x[i] - x[j] > R) j++; // j出界,j无法跳到i
while (hh <= tt && q[hh] < j) hh++; // j都出界了,单调队列也需要维护,把比j小的都干掉
// ③ 队列非空,队列头中保存的就是前面[L,R]范围内的分数最大值所对应的y号格子
if (hh <= tt) f[i] = f[q[hh]] + w[i]; // 再加上i号格子中的w[i]分值,就是总分值
// 时刻更新最大值
res = max(res, f[i]);
}
// 因为分值中存在负数,一旦是在半途中出现可以获取到的分值大于等于k时,就可以停止掉,表示已经找到了一个金币数量满足增加了自由度后,可以取得k这样的分值
return res >= k;
}
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> d >> k; // 格子数量,机器人每次可以跳的距离,想要拿到的分数
for (int i = 1; i <= n; i++) cin >> x[i] >> w[i]; // 起点到第i个格子的距离,第i个格子的分数
int L = 0, R = 1e9, ans = -1; // 二分的左右无脑边界值
/*
二分的最后结果有两种方式:
1、使用ans变量记录最终结果,ans初始化为-1,每次找到check()通过的mid,ans=mid,这样,如果最终有答案ans就记录的是答案,否则就记录的是-1。
2、不使用ans变量记录,而是最后再用!check(L)然后输出-1。
个人认为方法1更易理解。
*/
while (L < R) {
int mid = L + R >> 1;
if (check(mid)) {
R = mid;
ans = mid;
} else
L = mid + 1;
}
printf("%d\n", ans);
return 0;
}

View File

@@ -0,0 +1,70 @@
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 500010;
LL dist[N], w[N], f[N]; // 五年OI一场空,不开long long见祖宗
int n, d, k;
// 检查花费g个金币进行改造后,最高得分是否会超过k
bool check(int gold) {
// 机器人能够弹跳的范围[L,R]
int L = max(1, d - gold);
int R = d + gold;
// 注意:这里要初始化为负无穷
memset(f, 0xc0, sizeof f);
f[0] = 0;
for (int i = 1; i <= n; i++) {
// 从i的前一个格子开始,枚举到起点
for (int j = i - 1; j >= i - R; j--) {
// 剪枝,机器人从j号格子加上R还是法到达i号格子
// 那么从j号格子之前的格子也无法到达i号格子
if (dist[j] + R < dist[i]) break;
// 机器人从j号格子加上L还是超过了i号格子
// 那么继续尝试j号之前的格子
if (dist[j] + L > dist[i]) continue;
// 机器人从j号格子可以转移到i号格子
f[i] = max(f[i], f[j] + w[i]);
if (f[i] >= k) return true;
}
}
return false;
}
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> d >> k;
for (int i = 1; i <= n; i++) cin >> dist[i] >> w[i];
/*
二分金币的数量
二分时候左右端点是什么?
由于我们点的个数是500000个,要求分数的范围是[1,1e9],机器人初始时是2000的弹跳距离,
那么再结合我们花费金币后机器人弹跳范围,我们想要金币花费最少,假设每个点都得走过,
分数和才能达到最值,那么我们计算下来需要花费的平均金币数为2000,但实际上由于每个点的
分数分布不均匀,并且并不需要每一个点都经过,所以我们二分可以将左边界设为0,右边界设
为2010。
*/
int L = 0, R = 2010, ans = -1;
while (L < R) {
// 所有满足条件的情况都在mid的右边区间,
// 搜索右边区间最小值
// 适用二分搜索模板1
int mid = L + R >> 1;
if (check(mid)) {
ans = mid;
R = mid;
} else
L = mid + 1;
}
cout << ans << endl;
return 0;
}

View File

@@ -0,0 +1,71 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 510;
const int M = 110;
struct Node {
int x, y;
/*
Q1:ΪʲôҪ<C3B4><D2AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
A:<3A><>ΪҪDP<44><50><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD><EFBFBD>޺<EFBFBD>Ч<EFBFBD>ԡ<EFBFBD>Ҳ<EFBFBD><D2B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>ĵ<EFBFBD><C4B5><EFBFBD><EFBFBD>ԣ<EFBFBD><D4A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ң<EFBFBD><D2A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ<EFBFBD><D6B5><EFBFBD><EFBFBD><EFBFBD>ٱ<EFBFBD><D9B1>޸ģ<DEB8><C4A3><EFBFBD>Ҫ<EFBFBD><D2AA>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ˳<CFB5><CBB3>
Q2:<3A><>ʲô<CAB2><C3B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
A:<3A><><EFBFBD>԰<EFBFBD>x+y<><79><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҳ<EFBFBD><D2B2><EFBFBD>԰<EFBFBD><D4B0><EFBFBD>x,<2C><>y<EFBFBD>ķ<EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD>򣬶<EFBFBD><F2A3ACB6><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>ġ<EFBFBD>
1.<2E><>x+y<><79><EFBFBD><EFBFBD>
const bool operator<(const Node &t) {
return x + y < t.x + t.y;
}
*/
// 2.<2E>Ȱ<EFBFBD>x,<2C>ٰ<EFBFBD>y<EFBFBD><79><EFBFBD><EFBFBD>
const bool operator<(const Node &t) {
if (x == t.x) return y < t.y;
return x < t.x;
}
} a[N];
int f[N][M];
int ans;
int main() {
// <20>ļ<EFBFBD><C4BC><EFBFBD><EFBFBD><EFBFBD>
// freopen("point.in", "r", stdin);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> a[i].x >> a[i].y;
sort(a + 1, a + 1 + n);
// dp<64><70>ʼ<EFBFBD><CABC>,<2C><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD>ŵ<EFBFBD>ѡ<EFBFBD>У<EFBFBD><D0A3><EFBFBD><EFBFBD><EFBFBD>,ʹ<><CAB9><EFBFBD><EFBFBD>j<EFBFBD><6A><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>£<EFBFBD><C2A3><EFBFBD><EFBFBD>õ<EFBFBD><C3B5><EFBFBD><EEB3A4><EFBFBD>г<EFBFBD><D0B3><EFBFBD>
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
f[i][j] = j + 1; // <20><>i<EFBFBD><69><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ң<EFBFBD>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD>j<EFBFBD><6A><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>㣬<EFBFBD><E3A3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Թ<EFBFBD><D4B9><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EEB3A4><EFBFBD><EFBFBD>Ϊj+1<><31><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// ״̬ת<CCAC><D7AA>
for (int i = 1; i <= n; i++) // ö<><C3B6>ÿ<EFBFBD><C3BF>ԭʼ<D4AD><CABC>
for (int j = 1; j < i; j++) { // ö<><C3B6>ÿ<EFBFBD><C3BF>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>
if (a[i].x < a[j].x || a[i].y < a[j].y) continue; // <20><><EFBFBD><EFBFBD>j<EFBFBD><6A><EFBFBD><EFBFBD>i<EFBFBD><69><EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><C2B7><EFBFBD><EFBFBD><EFBFBD>Чת<D0A7><D7AA>
int d = a[i].x - a[j].x + a[i].y - a[j].y - 1; // j->i <20><>Ҫ<EFBFBD><D2AA><EFBFBD>ӵ<EFBFBD><D3B5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/*
<20><><EFBFBD><EFBFBD>j<EFBFBD><6A><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Щ<EFBFBD><D0A9>״̬ <20><><EFBFBD><EFBFBD>ת<EFBFBD>Ƶ<EFBFBD> i<><69><EFBFBD><EFBFBD>Щ<EFBFBD><D0A9>״̬<D7B4><CCAC>
˼<><CBBC>һ<EFBFBD><D2BB>f[j]<5D>Ķ<EFBFBD>ά״̬<D7B4><CCAC><EFBFBD><EFBFBD>Сֵ<D0A1><D6B5>0,<2C><><EFBFBD><EFBFBD>ֵ<EFBFBD><D6B5>m
<20><><EFBFBD><EFBFBD>f[j]<5D>Ķ<EFBFBD>ά״̬<D7B4>仯<EFBFBD><E4BBAF><EFBFBD>ǹ̶<C7B9><CCB6>ģ<EFBFBD><C4A3><EFBFBD>d
<20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫö<D2AA><C3B6>j<EFBFBD><6A><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD>ƵĶ<C6B5>ά״ֵ̬<CCAC><D6B5>0 ~ m-d
*/
for (int k = 0; k <= m - d; k++) {
// f[j][k]<5D><>ǰ<EFBFBD><C7B0>״̬<D7B4><CCAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>+dת<64>Ƶ<EFBFBD><C6B5>µ<EFBFBD>״̬
// f[j][k+d]+d+1 -> f[i][k+d]
f[i][k + d] = max(f[i][k + d], f[j][k] + d + 1);
// <20><>״̬ת<CCAC><D7AA><EFBFBD><EFBFBD><EFBFBD>ɺ<EFBFBD><C9BA><EFBFBD><EFBFBD>ռ<EFBFBD>һ<EFBFBD>´<EFBFBD><C2B4><EFBFBD>
// С<><D0A1>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>m<EFBFBD><6D><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>㣬<EFBFBD><E3A3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD>˵Ļ<CBB5><C4BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˷ѣ<CBB7><D1A3><EFBFBD>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD>룬
// <20>Ѷ<EFBFBD><D1B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ķ<EFBFBD><C4B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϳ<EFBFBD><CDBF><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>г<EFBFBD><D0B3><EFBFBD>
ans = max(ans, f[i][k + d] + m - k - d); // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EEB3A4><EFBFBD><EFBFBD>
}
}
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
printf("%d\n", ans);
return 0;
}

View File

@@ -0,0 +1,219 @@
### [$CSP-J$ $2022$] 上升点列
[P8816 [CSP-J 2022] 上升点列(民间数据)](https://www.luogu.com.cn/problem/P8816)
[前导知识练习 力扣 664. 奇怪的打印机](https://www.cnblogs.com/littlehb/p/16869777.html)
样例$1$输入解析
![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2022/12/2dc17e2354277c36a0153d24b915021c.png)
![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2022/12/355e3f69c3a6f89d990f9f8d8a50b5fd.png)
#### 动态规划解法
这种题目乍一看就能想到是 **$dp$** 或者 **二分**,从哪个先开始考虑都没问题
思考二分最后会发现不知道该从何入手,如果是二分长度,那不知道起点,二分起点,那也没有意义
再配合数据范围只有$500$,这种数据范围大概率就是$dp$,那么来思考$dp$
#### 状态表示
很容易想到$f[i][j]$表示到第$i$个节点,已经用掉了$j$个可添加点的 **最大长度**
#### 状态转移
状态有了,转移应该也很容易想到,对于第$i$个节点,无非就是枚举其他节点$j$,如果$j$在$i$的左下方即可进行转移,计算
$$\large d=a[i].x-a[j].x+a[i].y-a[j].y-1$$
即$j$到$i$ 需要使用 $d$ 个可添加点
现在的场景是从$j->i$,对于状态表示$f[i][?]$的一维已经确定是从$f[j][?]->f[i][??]$了,这两个问题间的转移是关键问题了:
那么枚举$f[j][k]$,即可得到方程
$$\large f[i][k+d]=max(f[j][k]+d+1)$$
最后答案即为
$$\large ans = max(ans, f[i][k + d] + m - k - d)$$
#### 注意
这里注意一个小细节,如果总共有 $m$ 个可添加点,只用了$k+d$个,多出来的$m-k-d$个直接加在最后即可,不要忘记这个,不过这个坑在第二组$sample$里就给出来了,基本不会有人踩
<font color='red' size=4><b>不把测试用例看完,画出来,理解掉的人是傻子!!!</b></font>
检查一下复杂度,是$O(n^2K)$,显然没有问题
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 510;
const int M = 110;
struct Node {
int x, y;
/*
Q1:为什么要排序?
A:因为要DP,需要解决无后效性。也就是有一定的单调性,从左向右,从上向下填表,填完的数值不能再被修改,需要有一定的依赖关系顺序
Q2:按什么来排序?
A:可以按x+y来排序,也可以按先x,后y的方式来排序,都是一样的。
1.按x+y排序
const bool operator<(const Node &t) {
return x + y < t.x + t.y;
}
*/
// 2.先按x,再按y排序
const bool operator<(const Node &t) {
if (x == t.x) return y < t.y;
return x < t.x;
}
} a[N];
int f[N][M];
int ans;
int main() {
//文件输入
// freopen("point.in", "r", stdin);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> a[i].x >> a[i].y;
sort(a + 1, a + 1 + n);
// dp初始化,最后一个点是以i号点选中,并且,使用了j个虚拟点情况下,获得的最长序列长度
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
f[i][j] = j + 1; //以i点结束,并且,使用了j个虚拟点,最起码可以构成一个最长长度为j+1的序列
//状态转移
for (int i = 1; i <= n; i++) //枚举每个原始点
for (int j = 1; j < i; j++) { //枚举每个前序点
if (a[i].x < a[j].x || a[i].y < a[j].y) continue; //如果j不在i的左下方,无效转移
int d = a[i].x - a[j].x + a[i].y - a[j].y - 1; // j->i 需要增加的虚拟点个数
/*
讨论j点的哪些子状态 可以转移到 i的哪些子状态?
思考一下f[j]的二维状态,最小值是0,最大值是m
现在f[j]的二维状态变化量是固定的,是d
所以需要枚举j的所有以转移的二维状态值:0 ~ m-d
*/
for (int k = 0; k <= m - d; k++) {
// f[j][k]是前序状态,可以通过+d转移到新的状态
// f[j][k+d]+d+1 -> f[i][k+d]
f[i][k + d] = max(f[i][k + d], f[j][k] + d + 1);
//在状态转移完成后,收集一下答案
//小坑一个:给你m个虚拟点,你最后没用了的话,就是浪费,因为最起码,
//把多出来的放在最后就可以增长序列长度
ans = max(ans, f[i][k + d] + m - k - d); //更新序列最长长度
}
}
//输出结果
printf("%d\n", ans);
return 0;
}
```
[最优子结构及 $dp$ 数组遍历方向的问题](https://zhuanlan.zhihu.com/p/100993613)
* $1$、遍历的过程中,所需的状态必须是已经计算出来的
* $2$、遍历的终点必须是存储结果的那个位置
#### 记忆化搜索解法I
```c++
#include <bits/stdc++.h>
const int N = 510;
//性能:第12号测试点,时间最长,65ms
/*
搜索代码:暴力枚举每次选择哪个点,能选就选,维护剩下几个自由点,加个记忆化即可通过
*/
using namespace std;
int n, m, ans;
int x[N], y[N]; //对于二维坐标,两个x,y数组,明显比使用struct的结构体数组方便,但是,不利用整体排序
int f[N][N]; //结果数组
//从u点出发,还有r个虚拟点可用,可以获得的最长序列长度是多少
int dfs(int u, int r) {
if (~f[u][r]) return f[u][r]; //计算过则直接返回
int ans = r + 1; //剩余r个虚拟点,再加上当前点u,最起码能有r+1点的序列长度
for (int i = 1; i <= n; i++) { //谁能做为我的后续点
if (u == i) continue; //自己不能做为自己的直接后续点
if (x[i] < x[u] || y[i] < y[u]) continue; //排除掉肯定不可能成为我后续点的点
int d = x[i] - x[u] + y[i] - y[u] - 1; // u->i之间缺少 多少个虚拟点
if (d > r) continue; //如果需要的虚拟点个数大于剩余的虚拟点个数,那么i 无法做为u的后续点
ans = max(ans, dfs(i, r - d) + d + 1); //在消耗了d个虚拟点之后,成功到达了i这个点;
//①已经取得的序列长度贡献u和d个虚拟点,共d+1个
//②问题转化为求未知部分:以i点为出发点,剩余虚拟点个数r-d个的情况下可以获取到的最长序列长度
}
return f[u][r] = ans; //记忆化
}
int main() {
//文件输入
// freopen("point.in", "r", stdin);
memset(f, -1, sizeof f);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
for (int i = 1; i <= n; i++) ans = max(ans, dfs(i, m));
printf("%d\n", ans);
return 0;
}
```
#### 记忆化搜索优化版本
```c++
#include <bits/stdc++.h>
const int N = 510;
//性能:第12号测试点,时间最长,22ms
/*
搜索代码:暴力枚举每次选择哪个点,能选就选,维护剩下几个自由点,加个记忆化即可通过
*/
using namespace std;
int n, m, ans;
int f[N][N]; //结果数组
struct Node {
int x, y;
const bool operator<(Node &t) const {
if (x == t.x) return y < t.y;
return x < t.x;
}
} a[N];
//从u点出发,还有r个虚拟点可用,可以获得的最长序列长度是多少
int dfs(int u, int r) {
if (~f[u][r]) return f[u][r]; //计算过则直接返回
int ans = r + 1; //剩余r个虚拟点,再加上当前点u,最起码能有r+1点的序列长度
for (int i = u + 1; i <= n; i++) { //谁能做为我的后续点
if (u == i) continue; //自己不能做为自己的直接后续点
if (a[i].x < a[u].x || a[i].y < a[u].y) continue; //排除掉肯定不可能成为我后续点的点
int d = a[i].x - a[u].x + a[i].y - a[u].y - 1; // u->i之间缺少 多少个虚拟点
if (d > r) continue; //如果需要的虚拟点个数大于剩余的虚拟点个数,那么i 无法做为u的后续点
ans = max(ans, dfs(i, r - d) + d + 1); //在消耗了d个虚拟点之后,成功到达了i这个点;
//①已经取得的序列长度贡献u和d个虚拟点,共d+1个
//②问题转化为求未知部分:以i点为出发点,剩余虚拟点个数r-d个的情况下可以获取到的最长序列长度
}
return f[u][r] = ans; //记忆化
}
int main() {
//文件输入
// freopen("point.in", "r", stdin);
memset(f, -1, sizeof f);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) ans = max(ans, dfs(i, m));
printf("%d\n", ans);
return 0;
}
```

View File

@@ -0,0 +1,162 @@
## [$AcWing$ $475$. 摆渡车](https://www.acwing.com/problem/content/description/477/)
### 一、题目描述
有$n$名同学要乘坐摆渡车从 人大附中 前往 人民大学,第 $i$ 位同学在第 $t_i$分钟去等车。只有一辆摆渡车在工作,但摆渡车容量可以视为无限大。摆渡车从人大附中出发, 把车上的同学送到人民大学,再回到人大附中(去接其他同学),这样往返一趟总共花费$m$分钟(同学上下车时间忽略不计)。摆渡车要将所有同学都送到人民大学。
凯凯很好奇,如果他能任意安排摆渡车出发的时间,那么这些同学的 **等车时间之和最小** 为多少呢?
**注意**:摆渡车回到人大附中后可以即刻出发。
**输入格式**
第一行包含两个正整数 $n,m$,以一个空格分开,分别代表等车人数和摆渡车往返一趟的时间。
第二行包含 $n$ 个正整数,相邻两数之间以一个空格分隔,第 $i$ 个非负整数 $t_i$代表第$i$个同学到达车站的时刻。
**输出格式**
输出一行,一个整数,表示所有同学等车时间之和的最小值(单位:分钟)。
**输入样例**
```cpp {.line-numbers}
5 5
11 13 1 5 5
```
**输出样例**
```cpp {.line-numbers}
4
```
### 二、解题思路
如果把时间看作一个数轴,那么所有同学到达车站的时刻就是数轴上的点,安排车辆的工作就是把数轴划分成若干个 **左开右闭** 的子区间,如下图所示:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310091646906.png)
例如测试样例中摆渡车安排在时刻$1、6、13$时,所有同学的等待时间之和最少,为$4$。一共安排了$3$次乘车,将数轴划分$3$段左开右闭的区间,分别是:$(0,1],(1,6],(6,13]$,每段长度都$≥m$,上图中$m=5$。
这样,等待时间之和就转换成了所有点到各自所属区间右边界的距离之和。
#### 状态表示
设$f[i]$表示数轴上对于前$i$个点,且最后一段的右边界为$i$,位于$(0,i]$的所有点到各自所属区间右边界的 **距离之和的最小值**。
#### 状态转移
设最后一段是$(j,i]$,而每段长度$i−j≥m$,则有$j≤i−m$。如果第$k$位同学到达时间$t[k]$属于区间$(j,i]$,即$j<t[k]≤i$,他到右边界$i$的距离是$i−t[k]$。那么累加所有属于区间$(j,i]$的点到$i$点距离之和,然后加上上一阶段的状态$f[j]$,就得到了状态转移方程:
$$\large f[i]=\min_{j≤i−m}(\sum_{j<t[k]≤i}(i−t[k])+f[j])$$
进一步思考
$$\sum_{j<t[k]≤i}(i−t[k])=(\sum_{j<t[k]≤i}{i})−(\sum_{j<t[k]≤i}{t[k]})$$
其中$\displaystyle \sum{i}$和$\displaystyle \sum{t[k]}$ **都可以通过前缀和** 计算出来。
$\displaystyle \sum{i}=(cnt[i]−cnt[j])×i$,$cnt[i]$表示区间$(0,i]$中同学的个数。
$\displaystyle \sum{t[k]}=sum[i]−sum[j]$,$sum[i]$表示区间$(0,i]$中所有同学到达的时间之和。
状态转移方程可以写成:
$$\large f[i]=\min_{j≤i−m}(cnt[i]−cnt[j])×i−(sum[i]−sum[j])+f[j]$$
#### 时间复杂度
$f[i]$表示的是在时间轴上对于前$i$个点的最优解,$i≤4×10^6$,所以时间复杂度为$O(10^{12})$。
#### 代码实现
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
// 通过了 10/20个数据
const int N = 4000010;
const int INF = 0x3f3f3f3f;
int f[N], cnt[N], sum[N];
int n, m; // 等车人数和摆渡车往返一趟的时间
int T; // T表示最后一个同学到达车站的时间
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int t; // 每个同学到达车站的时刻
cin >> t;
T = max(T, t); // 最后一个同学到达车站的时间
cnt[t]++; // t时刻引发的到达车站的人数,可能是多人
sum[t] += t; // t时刻引发的等待时间和,可能是多人
}
// 注意,这里应计算到最后一同学可能等到的时间
for (int i = 1; i < T + m; i++) {
cnt[i] += cnt[i - 1]; // 求人数的前缀和
sum[i] += sum[i - 1]; // 求时间的前缀和
}
for (int i = 1; i < T + m; i++) {
f[i] = i * cnt[i] - sum[i]; // 特殊处理i<m的情况
for (int j = 0; j <= i - m; j++)
f[i] = min(f[i], (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]) + f[j]);
}
// 取右边界取的漂亮
int ans = INF;
for (int i = T; i < T + m; i++) ans = min(ans, f[i]);
cout << ans << endl;
return 0;
}
```
### 三、算法优化
$DP$优化常用的两种方法:
- **剪去无用转移**
考虑区间$(j,i]$,在计算$f[i]$时,$j$从$0$开始枚举。其实可以缩小$j$的范围,从$i - 2 * m$开始枚举。因为当区间的长度$≥2m$时,可以安排摆渡车跑一个来回,等待时间不会变长。通过此性质,可剪去大量无用转移,状态转移方程更新为:
$$\large f[i]=\min_{i−2m<j≤i−m}(cnt[i]−cnt[j])×i−(sum[i]−sum[j])+f[j]$$
- **剪去无用状态**
对于某个阶段的状态$f[i]$,如果在区间$(i−m,i]$中没有任何点,那么状态$f[i]=f[i-m]$。因为区间中没有任何学生,可以不安排摆渡车,不会增加学生的等待时间。
优化后时间复杂度$O(nm^2+t)$。
#### 代码实现
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 4000010;
int ans = INF;
int cnt[N], sum[N], f[N];
int n, m; // 等车人数和摆渡车往返一趟的时间
int T; // T表示最后一个同学到达车站的时间
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int t;
cin >> t;
T = max(T, t);
cnt[t]++;
sum[t] += t;
}
// 注意,这里应计算到最后一同学可能等到的时间 T + m - 1
for (int i = 1; i < T + m; i++) {
cnt[i] += cnt[i - 1]; // 求人数的前缀和
sum[i] += sum[i - 1]; // 求时间的前缀和
}
for (int i = 1; i < T + m; i++) {
// 可以多通过6个测试点
if (i >= m && cnt[i] == cnt[i - m]) {
f[i] = f[i - m];
continue;
}
f[i] = i * cnt[i] - sum[i]; // 特殊处理i<m的情况
// 可以多通过4个测试点
for (int j = max(0, i - 2 * m + 1); j <= i - m; j++)
f[i] = min(f[i], (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]) + f[j]);
}
// 注意,最后一名同学上车的时刻在[T,T + m),求 其中最小值。
for (int i = T; i < T + m; i++) ans = min(ans, f[i]);
cout << ans << endl;
return 0;
}
```

View File

@@ -0,0 +1,37 @@
#include <bits/stdc++.h>
using namespace std;
// 通过了 10/20个数据
const int N = 4000010;
const int INF = 0x3f3f3f3f;
int f[N], cnt[N], sum[N];
int n, m; // 等车人数和摆渡车往返一趟的时间
int T; // T表示最后一个同学到达车站的时间
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int t; // 每个同学到达车站的时刻
cin >> t;
T = max(T, t); // 最后一个同学到达车站的时间
cnt[t]++; // t时刻引发的到达车站的人数,可能是多人
sum[t] += t; // t时刻引发的等待时间和,可能是多人
}
// 注意,这里应计算到最后一同学可能等到的时间
for (int i = 1; i < T + m; i++) {
cnt[i] += cnt[i - 1]; // 求人数的前缀和
sum[i] += sum[i - 1]; // 求时间的前缀和
}
for (int i = 1; i < T + m; i++) {
f[i] = i * cnt[i] - sum[i]; // 特殊处理i<m的情况
for (int j = 0; j <= i - m; j++)
f[i] = min(f[i], (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]) + f[j]);
}
// 取右边界取的漂亮
int ans = INF;
for (int i = T; i < T + m; i++) ans = min(ans, f[i]);
cout << ans << endl;
return 0;
}

View File

@@ -0,0 +1,45 @@
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 4000010;
int ans = INF;
int cnt[N], sum[N], f[N];
int n, m; // 等车人数和摆渡车往返一趟的时间
int T; // T表示最后一个同学到达车站的时间
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int t;
cin >> t;
T = max(T, t);
cnt[t]++;
sum[t] += t;
}
// 注意,这里应计算到最后一同学可能等到的时间 T + m - 1
for (int i = 1; i < T + m; i++) {
cnt[i] += cnt[i - 1]; // 求人数的前缀和
sum[i] += sum[i - 1]; // 求时间的前缀和
}
for (int i = 1; i < T + m; i++) {
// 可以多通过6个测试点
if (i >= m && cnt[i] == cnt[i - m]) {
f[i] = f[i - m];
continue;
}
f[i] = i * cnt[i] - sum[i]; // 特殊处理i<m的情况
// 可以多通过4个测试点
for (int j = max(0, i - 2 * m + 1); j <= i - m; j++)
f[i] = min(f[i], (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]) + f[j]);
}
// 注意,最后一名同学上车的时刻在[T,T + m),求其中最小值。
for (int i = T; i < T + m; i++) ans = min(ans, f[i]);
cout << ans << endl;
return 0;
}

View File

@@ -0,0 +1,19 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 110, mod = 1000007;
int n, m;
int f[N];
int main() {
cin >> n >> m;
f[0] = 1;
for (int i = 1; i <= n;i++){
int a;
cin>>a;
for (int j = m; j >= 0;j--)
for (int k = 1; k <= min(a, j);k++)
f[j] = (f[j] + f[j - k]) % mod;
}
cout << f[m] << endl;
return 0;
}