Codeforces Round 982 div2 个人题解(A~D2)
Dashboard - Codeforces Round 982 (Div. 2) - Codeforces
火车头
#define _CRT_SECURE_NO_WARNINGS 1
#include <algorithm>
#include <array>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <list>
#include <map>
#include <numeric>
#include <queue>
#include <random>
#include <set>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>
#define ft first
#define sd second
#define yes cout << "yes\n"
#define no cout << "no\n"
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define pb push_back
#define eb emplace_back
#define all(x) x.begin(), x.end()
#define all1(x) x.begin() + 1, x.end()
#define unq_all(x) x.erase(unique(all(x)), x.end())
#define unq_all1(x) x.erase(unique(all1(x)), x.end())
#define sort_all(x) sort(all(x))
#define sort1_all(x) sort(all1(x))
#define reverse_all(x) reverse(all(x))
#define reverse1_all(x) reverse(all1(x))
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL
#define RED cout << "\033[91m"
#define GREEN cout << "\033[92m"
#define YELLOW cout << "\033[93m"
#define BLUE cout << "\033[94m"
#define MAGENTA cout << "\033[95m"
#define CYAN cout << "\033[96m"
#define RESET cout << "\033[0m"
// 红色
#define DEBUG1(x) \
RED; \
cout << #x << " : " << x << endl; \
RESET;
// 绿色
#define DEBUG2(x) \
GREEN; \
cout << #x << " : " << x << endl; \
RESET;
// 蓝色
#define DEBUG3(x) \
BLUE; \
cout << #x << " : " << x << endl; \
RESET;
// 品红
#define DEBUG4(x) \
MAGENTA; \
cout << #x << " : " << x << endl; \
RESET;
// 青色
#define DEBUG5(x) \
CYAN; \
cout << #x << " : " << x << endl; \
RESET;
// 黄色
#define DEBUG6(x) \
YELLOW; \
cout << #x << " : " << x << endl; \
RESET;
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
// typedef __int128_t i128;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ld, ld> pdd;
typedef pair<ll, int> pli;
typedef pair<string, string> pss;
typedef pair<string, int> psi;
typedef pair<string, ll> psl;
typedef tuple<int, int, int> ti3;
typedef tuple<ll, ll, ll> tl3;
typedef tuple<ld, ld, ld> tld3;
typedef vector<bool> vb;
typedef vector<int> vi;
typedef vector<ll> vl;
typedef vector<string> vs;
typedef vector<pii> vpii;
typedef vector<pll> vpll;
typedef vector<pli> vpli;
typedef vector<pss> vpss;
typedef vector<ti3> vti3;
typedef vector<tl3> vtl3;
typedef vector<tld3> vtld3;
typedef vector<vi> vvi;
typedef vector<vl> vvl;
typedef queue<int> qi;
typedef queue<ll> ql;
typedef queue<pii> qpii;
typedef queue<pll> qpll;
typedef queue<psi> qpsi;
typedef queue<psl> qpsl;
typedef priority_queue<int> pqi;
typedef priority_queue<ll> pql;
typedef priority_queue<string> pqs;
typedef priority_queue<pii> pqpii;
typedef priority_queue<psi> pqpsi;
typedef priority_queue<pll> pqpll;
typedef priority_queue<psi> pqpsl;
typedef map<int, int> mii;
typedef map<int, bool> mib;
typedef map<ll, ll> mll;
typedef map<ll, bool> mlb;
typedef map<char, int> mci;
typedef map<char, ll> mcl;
typedef map<char, bool> mcb;
typedef map<string, int> msi;
typedef map<string, ll> msl;
typedef map<int, bool> mib;
typedef unordered_map<int, int> umii;
typedef unordered_map<ll, ll> uml;
typedef unordered_map<char, int> umci;
typedef unordered_map<char, ll> umcl;
typedef unordered_map<string, int> umsi;
typedef unordered_map<string, ll> umsl;
std::mt19937_64 rng(std::chrono::steady_clock::now().time_since_epoch().count());
template <typename T>
inline T read()
{
T x = 0;
int y = 1;
char ch = getchar();
while (ch > '9' || ch < '0')
{
if (ch == '-')
y = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * y;
}
template <typename T>
inline void write(T x)
{
if (x < 0)
{
putchar('-');
x = -x;
}
if (x >= 10)
{
write(x / 10);
}
putchar(x % 10 + '0');
}
/*#####################################BEGIN#####################################*/
void solve()
{
}
int main()
{
ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
// freopen("test.in", "r", stdin);
// freopen("test.out", "w", stdout);
int _ = 1;
std::cin >> _;
while (_--)
{
solve();
}
return 0;
}
/*######################################END######################################*/
// 链接:
A. Rectangle Arrangement
您正在为一个无限的正方形网格着色,其中所有单元格最初都是白色的。为此,您将获得 n n n 个印章。每个印章都是一个宽度为 w i w_i wi、高度为 h i h_i hi 的矩形。
您将使用每个印章恰好一次,将与网格上的印章大小相同的矩形涂成黑色。您不能旋转印章,并且对于每个单元格,印章必须完全覆盖它或根本不覆盖它。您可以在网格上的任何位置使用印章,即使印章区域覆盖的部分或全部单元格已经是黑色。
使用完所有印章后,您可以获得的黑色方块连接区域的周长的最小总和是多少?
输入
每个测试包含多个测试用例。第一行包含测试用例的数量
t
t
t
(
1
≤
t
≤
500
)
(1 \leq t \leq 500)
(1≤t≤500)。测试用例的描述如下。
每个测试用例的第一行包含一个整数 n n n ( 1 ≤ n ≤ 100 ) (1 \leq n \leq 100) (1≤n≤100)。
接下来的 n n n 行中的第 i i i 行包含两个整数 w i w_i wi 和 h i h_i hi ( 1 ≤ w i , h i ≤ 100 ) (1 \leq w_i, h_i \leq 100) (1≤wi,hi≤100)。
输出
对于每个测试用例,输出一个整数——使用完所有邮票后可以获得的黑色方块连通区域周长的最小和。
示例
输入
5
5
1 5
2 4
3 3
4 2
5 1
3
2 2
1 1
1 2
1
3 2
3
100 100
100 100
100 100
4
1 4
2 3
1 5
3 2
输出
20
8
10
400
16
提示
在第一个测试用例中,印章可以如左侧所示使用。每个印章用不同的颜色突出显示以便于识别。
在使用完这些印章后,形成一个黑色区域(如右侧所示),其周长为 20。可以证明,没有其他方式使用印章可以得到更低的总周长。
在第二个测试用例中,第二个和第三个印章可以完全放置在第一个印章内部,因此最小周长为 8。解题思路
解题思路
小学数学,观察样例发现总周长一定为$2\times (\text{max}(w)+\text{max}(h)) $
当然观察数据范围发现,也可以使用暴力模拟
代码实现
方法一
void solve()
{
int n;
cin >> n;
int max_w = 0, max_h = 0;
for (int i = 0; i < n; i++)
{
int w, h;
cin >> w >> h;
if (w > max_w)
max_w = w;
if (h > max_h)
max_h = h;
}
cout << 2 * (max_w + max_h) << "\n";
}
方法二
赛时写的方法二,想了下好像会被hack掉,有可能超时,有改成方法一了,结果赛后再交没超时,白丢分
void solve()
{
int n;
cin >> n;
vvi board(102, vi(102, 0));
for (int i = 0; i < n; i++)
{
int x;
int y;
cin >> x >> y;
// 把所有印章从左上角开始印
for (int j = 1; j <= x; j++)
{
for (int k = 1; k <= y; k++)
{
board[j][k] = 1;
}
}
}
int ans = 0;
for (int i = 1; i <= 100; i++)
{
for (int j = 1; j <= 100; j++)
{
if (board[i][j] == 1)
{
// 如果遇到一个方块盖了章,给答案加4,再减去和相邻方块也盖了章的数量
ans += 4;
ans -= board[i - 1][j] + board[i + 1][j] + board[i][j - 1] + board[i][j + 1];
}
}
}
cout << ans << endl;
}
B. Stalin Sort
斯大林排序是一种有趣的排序算法,旨在消除不合适的元素,而不是费心对它们进行正确排序,这使其具有 O ( n ) O(n) O(n) 的时间复杂度。
它的流程如下:从数组中的第二个元素开始,如果它严格小于前一个元素(忽略那些已经被删除的元素),则删除它。继续遍历数组,直到按非递减顺序排序。例如,数组 [ 1 , 4 , 2 , 3 , 6 , 5 , 5 , 7 , 7 ] [1,4,2,3,6,5,5,7,7] [1,4,2,3,6,5,5,7,7] 经过斯大林排序后变为 [ 1 , 4 , 6 , 7 , 7 ] [1,4,6,7,7] [1,4,6,7,7]。
如果您可以通过对任何子数组重复应用斯大林排序,按非递增顺序对数组进行排序,则我们将数组定义为易受攻击的。
给定一个包含 n n n 个整数的数组 a a a,确定必须从数组中删除的最小整数数量,以使其易受攻击。
输入
每个测试由多个测试用例组成。第一行包含一个整数
t
t
t
(
1
≤
t
≤
500
)
(1 \leq t \leq 500)
(1≤t≤500) — 测试用例的数量。后面是测试用例的描述。
每个测试用例的第一行包含一个整数 n n n ( 1 ≤ n ≤ 2000 ) (1 \leq n \leq 2000) (1≤n≤2000) — 数组的大小。
每个测试用例的第二行包含 n n n 个整数 a 1 , a 2 , … , a n a_1,a_2,…,a_n a1,a2,…,an ( 1 ≤ a i ≤ 1 0 9 ) (1 \leq a_i \leq 10^9) (1≤ai≤109)。
保证所有测试用例的 n n n 之和不超过 2000。
输出
对于每个测试用例,输出一个整数——必须从数组中删除的最小整数数量,以使其易受攻击。
示例
输入
6
7
3 6 4 9 2 5 2
5
5 4 4 2 2
8
2 2 4 4 6 6 10 10
1
1000
9
6 8 9 10 12 9 7 5 4
7
300000000 600000000 400000000 900000000 200000000 400000000 200000000
输出
2
0
6
0
4
2
提示
在第一个测试用例中,最优答案是删除数字 3 和 9。然后我们剩下
a
=
[
6
,
4
,
2
,
5
,
2
]
a=[6,4,2,5,2]
a=[6,4,2,5,2]。为了证明这个数组是易受攻击的,我们可以首先对子数组
[
4
,
2
,
5
]
[4,2,5]
[4,2,5] 应用斯大林排序,得到
a
=
[
6
,
4
,
5
,
2
]
a=[6,4,5,2]
a=[6,4,5,2],然后对子数组
[
6
,
4
,
5
]
[6,4,5]
[6,4,5] 应用斯大林排序,得到
a
=
[
6
,
2
]
a=[6,2]
a=[6,2],该数组是非递增的。
在第二个测试用例中,数组已经是非递增的,因此我们不需要删除任何整数。
解题思路
题目要求剩下的数组按照非递减排序,所以无论最后数组是什么情况,我们一定可以对它进行斯大林排序,使得它只剩一种数字。
因此,题目实际上可以转化成有两种操作
- 花费1代价删除任意数字
- 花费0代价删除 a j ( a j < a i , j > i ) a_j(a_j \lt a_i,j \gt i) aj(aj<ai,j>i)
所以我们可以枚举剩下的数字 a i a_i ai,它的花费 c o s t i = i − 1 + ∑ j = i + 1 n [ a j < a i ] cost_i=i-1+\sum_{j=i+1}^{n}[a_j\lt a_i] costi=i−1+∑j=i+1n[aj<ai]
由于 n ≤ 2000 n\le2000 n≤2000,我们可以直接枚举统计 a j < a i a_j\lt a_i aj<ai的数量
代码实现
方法一
void solve()
{
int n;
cin >> n;
vi a(n + 1);
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
int ans = inf;
for (int i = 1; i <= n; i++)
{
int cnt = 0;
for (int j = i + 1; j <= n; j++)
{
if (a[j] > a[i])
cnt++;
}
ans = min(ans, i - 1 + cnt);
}
cout << ans << endl;
}
方法二
对于 n ≤ 200000 n\le200000 n≤200000,方法一会超时,所有这时候我们可以倒着枚举 a i a_i ai,然后开一个对顶堆存储 a i a_i ai
void solve()
{
int n;
cin >> n;
vi a(n + 1);
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
int ans = inf;
priority_queue<int, vi, greater<int>> ge;
priority_queue<int> le;
for (int i = n; i >= 1; i--)
{
while (!ge.empty() && ge.top() <= a[i])
{
le.push(ge.top());
ge.pop();
}
while (!le.empty() && le.top() > a[i])
{
ge.push(le.top());
le.pop();
}
ans = min(ans, int(i - 1 + ge.size()));
ge.push(a[i]);
}
cout << ans << endl;
}
C. Add Zeros
给定一个数组 a a a,最初包含 n n n 个整数。在一次操作中,您必须执行以下操作:
选择一个位置
i
i
i,使得
1
<
i
≤
∣
a
∣
1<i\leq |a|
1<i≤∣a∣ 和
a
i
=
∣
a
∣
+
1
−
i
a_i=|a|+1−i
ai=∣a∣+1−i,其中
∣
a
∣
|a|
∣a∣ 是数组的当前大小。
将
i
−
1
i−1
i−1 个零附加到
a
a
a 的末尾。
在执行此操作多次后,数组
a
a
a 的最大可能长度是多少?
输入
每个测试包含多个测试用例。第一行包含测试用例的数量
t
t
t
(
1
≤
t
≤
1000
)
(1 \leq t \leq 1000)
(1≤t≤1000)。测试用例的描述如下。
每个测试用例的第一行包含 n n n ( 1 ≤ n ≤ 3 ⋅ 1 0 5 ) (1 \leq n \leq 3 \cdot 10^5) (1≤n≤3⋅105)——数组 a a a 的长度。
每个测试用例的第二行包含 n n n 个整数 a 1 , a 2 , … , a n a_1,a_2,…,a_n a1,a2,…,an ( 1 ≤ a i ≤ 1 0 12 ) (1 \leq a_i \leq 10^{12}) (1≤ai≤1012)。
保证所有测试用例的 n n n 之和不超过 3 ⋅ 1 0 5 3 \cdot 10^5 3⋅105。
输出
对于每个测试用例,输出一个整数——执行某些操作序列后可能的最大长度为
a
a
a。
示例
输入
4
5
2 4 6 2 5
5
5 4 4 5 1
4
6 8 2 3
1
1
输出
10
11
10
1
提示
在第一个测试用例中,我们可以首先选择
i
=
4
i=4
i=4,因为
a
4
=
5
+
1
−
4
=
2
a_4=5+1−4=2
a4=5+1−4=2。之后,数组变为
[
2
,
4
,
6
,
2
,
5
,
0
,
0
,
0
]
[2,4,6,2,5,0,0,0]
[2,4,6,2,5,0,0,0]。然后我们可以选择
i
=
3
i=3
i=3,因为
a
3
=
8
+
1
−
3
=
6
a_3=8+1−3=6
a3=8+1−3=6。之后,数组变为
[
2
,
4
,
6
,
2
,
5
,
0
,
0
,
0
,
0
,
0
]
[2,4,6,2,5,0,0,0,0,0]
[2,4,6,2,5,0,0,0,0,0],其长度为 10。可以证明没有其他操作序列会使最终数组更长。
在第二个测试用例中,我们可以选择 i = 2 i=2 i=2,然后 i = 3 i=3 i=3,再然后 i = 4 i=4 i=4。最终数组将是 [ 5 , 4 , 4 , 5 , 1 , 0 , 0 , 0 , 0 , 0 , 0 ] [5,4,4,5,1,0,0,0,0,0,0] [5,4,4,5,1,0,0,0,0,0,0],长度为 11。
解题思路
我们把题目给的式子转化一下,变成 ∣ a ∣ = a i + i − 1 |a|=a_i+i-1 ∣a∣=ai+i−1。
由于每个 a i a_i ai操作一次会使得 ∣ a ∣ |a| ∣a∣增加 i − 1 i-1 i−1,实际上就相当于每个 a i a_i ai都是对 a i + i − 1 a_i+i-1 ai+i−1到 a i + 2 i − 2 a_i+2i-2 ai+2i−2建了一条边,点权为数组长度。
所以我们可以按照上面的规则进行建图,对这个图从 n n n进行 d f s dfs dfs, d f s dfs dfs过程中维护一下遇到的最大点权,即为答案。
记得要加 v i s vis vis数组,不然会超时。
代码实现
void solve()
{
ll n;
cin >> n;
vl a(n + 1);
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
map<ll, vl> adj;
for (ll i = 1; i <= n; i++)
{
adj[a[i] + i - 1].pb(a[i] + 2 * (i - 1));
}
ll ans = n;
mlb vis;
function<void(ll, ll)> dfs = [&](ll u, ll fa)
{
ans = max(ans, u);
vis[u] = 1;
for (auto &v : adj[u])
{
if (v == fa)
continue;
if (vis[v])
continue;
dfs(v, u);
}
};
dfs(n, 0);
cout << ans << endl;
}
D1. The Endspeaker (Easy Version)
这是该问题的简单版本。唯一的区别是,您只需要在此版本中输出最小的总操作成本。您必须解决两个版本才能破解。
您将获得一个长度为 n n n 的数组 a a a 和一个长度为 m m m 的数组 b b b(对于所有 1 ≤ i < m 1 \leq i < m 1≤i<m,其值为 b i > b i + 1 b_i > b_{i+1} bi>bi+1)。最初, k k k 的值为 1。您的目标是通过重复执行以下两个操作之一来使数组 a a a 为空:
类型 1 — 如果
k
k
k 的值小于
m
m
m 且数组
a
a
a 不为空,则可以将
k
k
k 的值增加 1。这不会产生任何成本。
类型 2 — 删除数组
a
a
a 的非空前缀,使其总和不超过
b
k
b_k
bk。这会产生
m
−
k
m−k
m−k 的成本。
您需要最小化操作的总成本,以使数组 a a a 为空。如果通过任何操作序列都无法做到这一点,则输出 − 1 −1 −1。否则,输出操作的最小总成本。
输入
每个测试包含多个测试用例。第一行包含测试用例的数量
t
t
t
(
1
≤
t
≤
1000
)
(1 \leq t \leq 1000)
(1≤t≤1000)。测试用例的描述如下。
每个测试用例的第一行包含两个整数 n n n 和 m m m ( 1 ≤ n , m ≤ 3 ⋅ 1 0 5 , 1 ≤ n ⋅ m ≤ 3 ⋅ 1 0 5 ) (1 \leq n,m \leq 3 \cdot 10^5, 1 \leq n \cdot m \leq 3 \cdot 10^5) (1≤n,m≤3⋅105,1≤n⋅m≤3⋅105)。
第二行包含 n n n 个整数 a 1 , a 2 , … , a n a_1,a_2,…,a_n a1,a2,…,an ( 1 ≤ a i ≤ 1 0 9 ) (1 \leq a_i \leq 10^9) (1≤ai≤109)。
第三行包含 m m m 个整数 b 1 , b 2 , … , b m b_1,b_2,…,b_m b1,b2,…,bm ( 1 ≤ b i ≤ 1 0 9 ) (1 \leq b_i \leq 10^9) (1≤bi≤109)。
还保证所有 1 ≤ i < m 1 \leq i < m 1≤i<m 的结果是 b i > b i + 1 b_i > b_{i+1} bi>bi+1。
保证所有测试用例的 n ⋅ m n \cdot m n⋅m 之和不超过 3 ⋅ 1 0 5 3 \cdot 10^5 3⋅105。
输出
对于每个测试用例,如果有可能使
a
a
a 为空,则输出操作的最小总成本。如果没有可能的操作序列使
a
a
a 为空,则输出单个整数
−
1
−1
−1。
示例
输入
5
4 2
9 3 4 3
11 7
1 2
20
19 18
10 2
2 5 2 1 10 3 2 9 9 6
17 9
10 11
2 2 2 2 2 2 2 2 2 2
20 18 16 14 12 10 8 6 4 2 1
1 6
10
32 16 8 4 2 1
输出
1
-1
2
10
4
提示
在第一个测试用例中,最优操作序列的总成本为 1,过程如下:
- 执行类型 2 的操作,选择前缀为 [ 9 ] [9] [9],成本为 1。
- 执行类型 1 的操作, k k k 现在为 2,成本为 0。
- 执行类型 2 的操作,选择前缀为 [ 3 , 4 ] [3,4] [3,4],成本为 0。
- 执行类型 2 的操作,选择前缀为 [ 3 ] [3] [3],成本为 0。
数组 a a a 现在为空,所有操作的总成本为 1。
在第二个测试用例中,由于 a 1 > b 1 a_1 > b_1 a1>b1,无法删除任何前缀,因此数组 a a a 不能通过任何操作序列变为空。
解题思路
观察题目,发现是使用多种操作完成一个目标求最小代价,考虑dp
我们设 d p [ i ] [ j ] dp[i][j] dp[i][j]为对于前 i i i个数字,使用第 j j j总操作时的最小代价。
易得状态转移方程
d
p
[
i
]
[
j
]
=
min
{
d
p
[
p
]
[
k
]
}
+
m
−
j
,
sum
[
p
,
i
]
≤
b
[
j
]
,
1
≤
k
≤
j
,
1
≤
p
≤
i
dp[i][j]= \text{min}\{dp[p][k]\}+m-j,\text{sum}[p,i]\le b[j],1\le k\le j,1\le p\le i
dp[i][j]=min{dp[p][k]}+m−j,sum[p,i]≤b[j],1≤k≤j,1≤p≤i
其中
sum
[
p
,
i
]
\text{sum}[p,i]
sum[p,i]数组
a
p
∼
a
i
a_p \sim a_i
ap∼ai的区间和
根据状态转移方程写出递推过程
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int p = i;
while (p >= 1 && pre[i] - pre[p - 1] <= b[j])
{
p--;
}
p++;
for (int k = 1; k <= j; k++)
{
dp[i][j] = min(dp[i][j], dp[p - 1][k] + m - j);
}
}
}
发现时间复杂度为 O ( n m × max ( n , m ) ) O(nm\times \text{max}(n,m)) O(nm×max(n,m)),在 n n n或 m m m特别大的情况下肯定超时。
考虑优化。
优化一
对于 p p p的位置,我们可以使用二分优化枚举
function<int(int, ll)> bianrySearch = [&](int x, ll val) -> int
{
int p = x;
int l = 1, r = x;
while (l < r)
{
int mid = (l + r) >> 1;
if (pre[x] - pre[mid - 1] <= val)
{
r = mid;
p = mid;
}
else
{
l = mid + 1;
}
}
return p;
};
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int l = 1, r = i;
int p = bianrySearch(i, b[j]);
for (int k = 1; k <= j; k++)
{
dp[i][j] = min(dp[i][j], dp[p - 1][k] + m - j);
}
}
}
时间复杂度降为 O ( n m 2 ) O(nm^2) O(nm2),在 m m m特别大的时候仍会超时
优化二
对 k k k的枚举,我们实际上是在查询区间 [ 1 , j ] [1,j] [1,j]的最小 d p [ i ] [ j ] dp[i][j] dp[i][j]值,所以我们可以使用树状数组存储 d p [ i ] [ j ] dp[i][j] dp[i][j]。
树状数组实现
template <typename T>
struct Fenwick
{
int n;
vector<T> a;
Fenwick(int n_ = 0) { init(n_); }
void init(int n_) { n = n_, a.assign(n + 1, T{inf}); }
void add(int x, const T &v)
{
for (int i = x; i <= n; i += i & -i)
a[i] = min(a[i], v);
}
T query(int x)
{
T res{inf};
for (int i = x; i > 0; i -= i & -i)
res = min(res, a[i]);
return res;
}
};
递推过程
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
if(b[j]<a[i])// 不能删除时跳过
continue;
int p = bianrySearch(i, b[j]);
ll mn = tr[p - 1].query(j);// 使用树状数组查询,查询1<=k<=j中最小的dp[i][k]
dp[i][j] = min(dp[i][j], mn + m - j);// 更新dp[i][j]
tr[i].add(j, dp[i][j]);// 将dp[i][j]插入树状数组中,以便后续查询
}
}
时间复杂度 O ( n m × max ( log n , log m ) ) O(nm\times \text{max}(\log n,\log m)) O(nm×max(logn,logm))
这时已经可以通过该题目了,但是还可以进行优化
优化三
考虑 j j j只能由 j − 1 j-1 j−1转移过来,因此我们可以在最外层循环枚举 j j j,使用滚动数组优化掉树状数组
for (int j = 1; j <= m; j++)
{
for (int i = 1; i <= n; i++)
{
if (b[j] < a[i])
continue;
int p = bianrySearch(i, b[j]);
dp[i] = min(dp[i], dp[p - 1] + m - j);
}
}
时间复杂度为 O ( n m log n ) O(nm\log n) O(nmlogn)
优化四
观察发现,在求 p p p的过程中,我们将其转成一个大小为 b j b_j bj的窗口在数组 a a a上移动,其中左边界为 p p p,右边界为 i i i,这样我们就优化掉了二分
for (int j = 1; j <= m; j++)
{
int l = 0, r = 1;
while (r <= n)
{
while (l < r && pre[r] - pre[l] > b[j])
l++;
if (l < r)
dp[r] = min(dp[r], dp[l] + m - j);
r++;
}
}
时间复杂度为 O ( n m ) O(nm) O(nm)
代码实现
void solve()
{
int n, m;
cin >> n >> m;
vector<ll> a(n + 1);
vector<ll> pre(n + 1);
ll max_a = 0;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
max_a = max(max_a, a[i]);
pre[i] = pre[i - 1] + a[i];
}
vl b(m + 1);
for (int i = 1; i <= m; i++)
{
cin >> b[i];
}
if (max_a > b[1])
{
cout << "-1\n";
return;
}
vector<ll> dp(n + 1, infll);
dp[0] = 0;
for (int j = 1; j <= m; j++)
{
int l = 0, r = 1;
while (r <= n)
{
while (l < r && pre[r] - pre[l] > b[j])
l++;
if (l < r)
dp[r] = min(dp[r], dp[l] + m - j);
r++;
}
}
cout << dp[n] << "\n";
}
D2. The Endspeaker (Hard Version)
这是该问题的难版本。唯一的区别是,您还需要在此版本中输出最佳序列的数量。
解题思路
顺着 D1 \text{D1} D1版本最后优化的思路,我们可以维护一个共享右边界的双重滑动窗口,滑动窗口大小为 b [ j ] b[j] b[j]
设 d p [ i ] dp[i] dp[i]为选择前 i i i个数字时的最小花费, c n t [ i ] cnt[i] cnt[i]为选择前 i i i个数字最小花费情况下的操作数量。
设 l l l为第一左边界, L L L为第二左边界, l l l负责维护 d p [ r ] dp[r] dp[r]从哪里转移过来, L L L负责 c n t [ r ] cnt[r] cnt[r]从哪里转移过来。
对于 d p [ r ] dp[r] dp[r]来说,转移方程和 D1 \text{D1} D1一致,对于 c n t [ r ] cnt[r] cnt[r]来说, c n t [ r ] = ∑ L = l R , d p [ R ] = d p [ l ] c n t [ L ] cnt[r]=\sum_{L=l}^{R,dp[R]=dp[l]}cnt[L] cnt[r]=∑L=lR,dp[R]=dp[l]cnt[L]
代码实现
const ll mod = 1e9 + 7;
void solve()
{
int n, m;
cin >> n >> m;
vector<ll> a(n + 1);
vector<ll> pre(n + 1);
ll max_a = 0;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
max_a = max(max_a, a[i]);
pre[i] = pre[i - 1] + a[i];
}
vector<ll> b(m + 1);
for (int i = 1; i <= m; i++)
{
cin >> b[i];
}
if (max_a > b[1])
{
cout << "-1\n";
return;
}
vector<ll> dp(n + 1, inf);
dp[0] = 0;
vector<ll> cnt(n + 1, 0);
cnt[0] = 1;
for (int j = 1; j <= m; j++)
{
int l = 0, r = 1;
int L = 0;
ll sum = 0;
while (r <= n)
{
// 窗口大小大于b[j],窗口内的序列都不可以用,要全部丢弃
while (l < r && pre[r] - pre[l] > b[j])
{
sum = (sum + mod - cnt[l]) % mod;
l++;
}
// 如果第二左边界小于了第一左边界,说明指向的右边界不同了,要将sum重置
if (L < l)
{
L = l;
sum = 0;
}
// 将共同左边界的操作序列数量全部记录
while (L < r && dp[L] == dp[l])
{
sum = (sum + cnt[L]) % mod;
L++;
}
if (l < r)
{
ll cost = dp[l] + m - j;
if (cost < dp[r])
// 如果花费更少,更新dp和cnt
{
dp[r] = cost;
cnt[r] = sum;
}
else if (cost == dp[r]) // 花费相同,cnt加上sum
{
cnt[r] = (cnt[r] + sum) % mod;
}
}
r++;
}
}
cout << dp[n] << " " << cnt[n] << "\n";
}
赛时把D1 dp的i和j写反了,找这个bug找了快一个小时,麻了,后面想到正解没时间写了,结束后三分钟写完了。太菜了,这几场掉大分。