Codeforces Round 982 div2 个人题解(A~D2)

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) (1t500)。测试用例的描述如下。

每个测试用例的第一行包含一个整数 n n n ( 1 ≤ n ≤ 100 ) (1 \leq n \leq 100) (1n100)

接下来的 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) (1wi,hi100)

输出
对于每个测试用例,输出一个整数——使用完所有邮票后可以获得的黑色方块连通区域周长的最小和。

示例
输入

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) (1t500) — 测试用例的数量。后面是测试用例的描述。

每个测试用例的第一行包含一个整数 n n n ( 1 ≤ n ≤ 2000 ) (1 \leq n \leq 2000) (1n2000) — 数组的大小。

每个测试用例的第二行包含 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) (1ai109)

保证所有测试用例的 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=i1+j=i+1n[aj<ai]

由于 n ≤ 2000 n\le2000 n2000,我们可以直接枚举统计 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 n200000,方法一会超时,所有这时候我们可以倒着枚举 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<ia a i = ∣ a ∣ + 1 − i a_i=|a|+1−i ai=a+1i,其中 ∣ a ∣ |a| a 是数组的当前大小。
i − 1 i−1 i1 个零附加到 a a a 的末尾。
在执行此操作多次后,数组 a a a 的最大可能长度是多少?

输入
每个测试包含多个测试用例。第一行包含测试用例的数量 t t t ( 1 ≤ t ≤ 1000 ) (1 \leq t \leq 1000) (1t1000)。测试用例的描述如下。

每个测试用例的第一行包含 n n n ( 1 ≤ n ≤ 3 ⋅ 1 0 5 ) (1 \leq n \leq 3 \cdot 10^5) (1n3105)——数组 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}) (1ai1012)

保证所有测试用例的 n n n 之和不超过 3 ⋅ 1 0 5 3 \cdot 10^5 3105

输出
对于每个测试用例,输出一个整数——执行某些操作序列后可能的最大长度为 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+14=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+13=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+i1

由于每个 a i a_i ai操作一次会使得 ∣ a ∣ |a| a增加 i − 1 i-1 i1,实际上就相当于每个 a i a_i ai都是对 a i + i − 1 a_i+i-1 ai+i1 a i + 2 i − 2 a_i+2i-2 ai+2i2建了一条边,点权为数组长度。

所以我们可以按照上面的规则进行建图,对这个图从 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 1i<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 mk 的成本。

您需要最小化操作的总成本,以使数组 a a a 为空。如果通过任何操作序列都无法做到这一点,则输出 − 1 −1 1。否则,输出操作的最小总成本。

输入
每个测试包含多个测试用例。第一行包含测试用例的数量 t t t ( 1 ≤ t ≤ 1000 ) (1 \leq t \leq 1000) (1t1000)。测试用例的描述如下。

每个测试用例的第一行包含两个整数 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) (1n,m3105,1nm3105)

第二行包含 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) (1ai109)

第三行包含 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) (1bi109)

还保证所有 1 ≤ i < m 1 \leq i < m 1i<m 的结果是 b i > b i + 1 b_i > b_{i+1} bi>bi+1

保证所有测试用例的 n ⋅ m n \cdot m nm 之和不超过 3 ⋅ 1 0 5 3 \cdot 10^5 3105

输出
对于每个测试用例,如果有可能使 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,过程如下:

  1. 执行类型 2 的操作,选择前缀为 [ 9 ] [9] [9],成本为 1。
  2. 执行类型 1 的操作, k k k 现在为 2,成本为 0。
  3. 执行类型 2 的操作,选择前缀为 [ 3 , 4 ] [3,4] [3,4],成本为 0。
  4. 执行类型 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]}+mj,sum[p,i]b[j],1kj,1pi
其中 sum [ p , i ] \text{sum}[p,i] sum[p,i]数组 a p ∼ a i a_p \sim a_i apai的区间和

根据状态转移方程写出递推过程

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 j1转移过来,因此我们可以在最外层循环枚举 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找了快一个小时,麻了,后面想到正解没时间写了,结束后三分钟写完了。太菜了,这几场掉大分。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值