Codeforces Round 982 div2 個人題解(A~D1)
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\) 個印章。每個印章都是一個寬度為 \(w_i\)、高度為 \(h_i\) 的矩形。
您將使用每個印章恰好一次,將與網格上的印章大小相同的矩形塗成黑色。您不能旋轉印章,並且對於每個單元格,印章必須完全覆蓋它或根本不覆蓋它。您可以在網格上的任何位置使用印章,即使印章區域覆蓋的部分或全部單元格已經是黑色。
使用完所有印章後,您可以獲得的黑色方塊連線區域的周長的最小總和是多少?
輸入
每個測試包含多個測試用例。第一行包含測試用例的數量 \(t\) \((1 \leq t \leq 500)\)。測試用例的描述如下。
每個測試用例的第一行包含一個整數 \(n\) \((1 \leq n \leq 100)\)。
接下來的 \(n\) 行中的第 \(i\) 行包含兩個整數 \(w_i\) 和 \(h_i\) \((1 \leq w_i, h_i \leq 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)\) 的時間複雜度。
它的流程如下:從陣列中的第二個元素開始,如果它嚴格小於前一個元素(忽略那些已經被刪除的元素),則刪除它。繼續遍歷陣列,直到按非遞減順序排序。例如,陣列 \([1,4,2,3,6,5,5,7,7]\) 經過斯大林排序後變為 \([1,4,6,7,7]\)。
如果您可以透過對任何子陣列重複應用斯大林排序,按非遞增順序對陣列進行排序,則我們將陣列定義為易受攻擊的。
給定一個包含 \(n\) 個整數的陣列 \(a\),確定必須從陣列中刪除的最小整數數量,以使其易受攻擊。
輸入
每個測試由多個測試用例組成。第一行包含一個整數 \(t\) \((1 \leq t \leq 500)\) — 測試用例的數量。後面是測試用例的描述。
每個測試用例的第一行包含一個整數 \(n\) \((1 \leq n \leq 2000)\) — 陣列的大小。
每個測試用例的第二行包含 \(n\) 個整數 \(a_1,a_2,…,a_n\) \((1 \leq a_i \leq 10^9)\)。
保證所有測試用例的 \(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]\)。為了證明這個陣列是易受攻擊的,我們可以首先對子陣列 \([4,2,5]\) 應用斯大林排序,得到 \(a=[6,4,5,2]\),然後對子陣列 \([6,4,5]\) 應用斯大林排序,得到 \(a=[6,2]\),該陣列是非遞增的。
在第二個測試用例中,陣列已經是非遞增的,因此我們不需要刪除任何整數。
解題思路
題目要求剩下的陣列按照非遞減排序,所以無論最後陣列是什麼情況,我們一定可以對它進行斯大林排序,使得它只剩一種數字。
因此,題目實際上可以轉化成有兩種操作
- 花費1代價刪除任意數字
- 花費0代價刪除\(a_j(a_j \lt a_i,j \gt i)\)
所以我們可以列舉剩下的數字\(a_i\),它的花費\(cost_i=i-1+\sum_{j=i+1}^{n}[a_j\lt a_i]\)
由於\(n\le2000\),我們可以直接列舉統計\(a_j\lt a_i\)的數量
程式碼實現
方法一
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\le200000\),方法一會超時,所有這時候我們可以倒著列舉\(a_i\),然後開一個對頂堆儲存\(a_i\)
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\),最初包含 \(n\) 個整數。在一次操作中,您必須執行以下操作:
選擇一個位置 \(i\),使得 \(1<i\leq |a|\) 和 \(a_i=|a|+1−i\),其中 \(|a|\) 是陣列的當前大小。
將 \(i−1\) 個零附加到 \(a\) 的末尾。
在執行此操作多次後,陣列 \(a\) 的最大可能長度是多少?
輸入
每個測試包含多個測試用例。第一行包含測試用例的數量 \(t\) \((1 \leq t \leq 1000)\)。測試用例的描述如下。
每個測試用例的第一行包含 \(n\) \((1 \leq n \leq 3 \cdot 10^5)\)——陣列 \(a\) 的長度。
每個測試用例的第二行包含 \(n\) 個整數 \(a_1,a_2,…,a_n\) \((1 \leq a_i \leq 10^{12})\)。
保證所有測試用例的 \(n\) 之和不超過 \(3 \cdot 10^5\)。
輸出
對於每個測試用例,輸出一個整數——執行某些操作序列後可能的最大長度為 \(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\),因為 \(a_4=5+1−4=2\)。之後,陣列變為 \([2,4,6,2,5,0,0,0]\)。然後我們可以選擇 \(i=3\),因為 \(a_3=8+1−3=6\)。之後,陣列變為 \([2,4,6,2,5,0,0,0,0,0]\),其長度為 10。可以證明沒有其他操作序列會使最終陣列更長。
在第二個測試用例中,我們可以選擇 \(i=2\),然後 \(i=3\),再然後 \(i=4\)。最終陣列將是 \([5,4,4,5,1,0,0,0,0,0,0]\),長度為 11。
解題思路
我們把題目給的式子轉化一下,變成\(|a|=a_i+i-1\)。
由於每個\(a_i\)操作一次會使得\(|a|\)增加\(i-1\),實際上就相當於每個\(a_i\)都是對\(a_i+i-1\)到\(a_i+2i-2\)建了一條邊,點權為陣列長度。
所以我們可以按照上面的規則進行建圖,對這個圖從\(n\)進行\(dfs\),\(dfs\)過程中維護一下遇到的最大點權,即為答案。
記得要加\(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\) 的陣列 \(a\) 和一個長度為 \(m\) 的陣列 \(b\)(對於所有 \(1 \leq i < m\),其值為 \(b_i > b_{i+1}\))。最初,\(k\) 的值為 1。您的目標是透過重複執行以下兩個操作之一來使陣列 \(a\) 為空:
型別 1 — 如果 \(k\) 的值小於 \(m\) 且陣列 \(a\) 不為空,則可以將 \(k\) 的值增加 1。這不會產生任何成本。
型別 2 — 刪除陣列 \(a\) 的非空字首,使其總和不超過 \(b_k\)。這會產生 \(m−k\) 的成本。
您需要最小化操作的總成本,以使陣列 \(a\) 為空。如果透過任何操作序列都無法做到這一點,則輸出 \(−1\)。否則,輸出操作的最小總成本。
輸入
每個測試包含多個測試用例。第一行包含測試用例的數量 \(t\) \((1 \leq t \leq 1000)\)。測試用例的描述如下。
每個測試用例的第一行包含兩個整數 \(n\) 和 \(m\) \((1 \leq n,m \leq 3 \cdot 10^5, 1 \leq n \cdot m \leq 3 \cdot 10^5)\)。
第二行包含 \(n\) 個整數 \(a_1,a_2,…,a_n\) \((1 \leq a_i \leq 10^9)\)。
第三行包含 \(m\) 個整數 \(b_1,b_2,…,b_m\) \((1 \leq b_i \leq 10^9)\)。
還保證所有 \(1 \leq i < m\) 的結果是 \(b_i > b_{i+1}\)。
保證所有測試用例的 \(n \cdot m\) 之和不超過 \(3 \cdot 10^5\)。
輸出
對於每個測試用例,如果有可能使 \(a\) 為空,則輸出操作的最小總成本。如果沒有可能的操作序列使 \(a\) 為空,則輸出單個整數 \(−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]\),成本為 1。
- 執行型別 1 的操作,\(k\) 現在為 2,成本為 0。
- 執行型別 2 的操作,選擇字首為 \([3,4]\),成本為 0。
- 執行型別 2 的操作,選擇字首為 \([3]\),成本為 0。
陣列 \(a\) 現在為空,所有操作的總成本為 1。
在第二個測試用例中,由於 \(a_1 > b_1\),無法刪除任何字首,因此陣列 \(a\) 不能透過任何操作序列變為空。
解題思路
觀察題目,發現是使用多種操作完成一個目標求最小代價,考慮dp
我們設\(dp[i][j]\)為對於前\(i\)個數字,使用第\(j\)總操作時的最小代價。
易得狀態轉移方程
其中\(\text{sum}[p,i]\)陣列\(a\)的區間和
根據狀態轉移方程寫出遞推式
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(nm\times \text{max}(n,m))\),在\(n\)或\(m\)特別大的情況下肯定超時。
考慮最佳化。
對於\(p\)的位置,我們可以使用二分最佳化列舉
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
int l = 1, r = i;
int p = i;
while (l < r)
{
int mid = (l + r) >> 1;
if (pre[i] - pre[mid - 1] <= b[j])
{
r = mid;
p = mid;
}
else
{
l = mid + 1;
}
}
for (int k = 1; k <= j; k++)
{
dp[i][j] = min(dp[i][j], dp[p - 1][k] + m - j);
}
}
}
時間複雜度降為\(O(nm^2)\),在\(m\)特別大的時候仍會超時
對\(k\)的列舉,我們實際上是在查詢區間\([1,j]\)的最小\(dp[i][j]\)值,所以我們可以使用樹狀陣列儲存\(dp[i][j]\)。
時間複雜度\(O(nm\times \text{max}(\log n,\log m))\)
程式碼實現
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{infll});
}
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 ans{infll};
for (int i = x; i > 0; i -= i & -i)
{
ans = min(ans, a[i]);
}
return ans;
}
};
void solve()
{
int n, m;
cin >> n >> m;
vl a(n + 1);
vl 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];
}
// 如果陣列a中的最大值大於b[1],無法把a陣列刪完,輸出-1
if (max_a > b[1])
{
cout << "-1\n";
return;
}
// 初始化動態規劃陣列dp,dp[i][j]表示刪除前i個元素,使用第j個操作時的最小成本
vvl dp(n + 1, vl(m + 1, infll));
// 對於刪除0個元素的情況,成本為0
for (int i = 1; i <= m; i++)
{
dp[0][i] = 0;
}
// 建立樹狀陣列tr
vector<Fenwick<ll>> tr(n + 1, Fenwick<ll>(m));
tr[0].add(1, 0); // 初始狀態,刪除0個元素成本設定為0
// 動態規劃狀態轉移
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
// 如果當前元素大於b[j],無法繼續刪除,跳出迴圈
if (a[i] > b[j])
break;
// 二分查詢,找到可以一次性刪除的最遠位置p
int l = 1, r = i;
int p = i;
while (l < r)
{
int mid = (l + r) >> 1;
if (pre[i] - pre[mid - 1] <= b[j])
{
r = mid;
p = mid;
}
else
{
l = mid + 1;
}
}
// 使用樹狀陣列查詢,查詢1<=k<=j中最小的dp[i][k]
ll mn = tr[p - 1].query(j);
// 更新dp[i][j]
dp[i][j] = min(dp[i][j], mn + m - j);
// 將dp[i][j]插入樹狀陣列樹中,以便後續查詢
tr[i].add(j, dp[i][j]);
}
}
// 計算最終答案,即dp[n][i]的最小值
ll ans = infll;
for (int i = 1; i <= m; i++)
{
ans = min(ans, dp[n][i]);
}
cout << ans << "\n";
}
賽時把D1 dp的i和j寫反了,找這個bug找了快一個小時,麻了,後面想到正解沒時間寫了,結束後三分鐘寫完了。太菜了,這幾場掉大分。