前言
手速6題,可惜第四題磨了幾個小時沒磨出來,多做一題就金了,還是實力差了點,最後銀牌前列。下面的題解是根據程式碼回憶大概的題意,主要是給出來賽時的參考程式碼
A.狀壓
題意:
學校集訓隊總的有 \(n\) 個人,保證 \(n\) 是3的倍數,每個人有個人實力 \(a_i\), 每兩個人之間有配合程度 \(b_{ij}\),每個人可以選擇掛機或者不掛機,隊伍有不同的總權值(題目中把所有可能的搭配都給出來了), 問合理安排下,最多使得所有隊伍的權值和最大是多少。
分析
看到資料範圍很顯然是考慮狀壓DP,二進位制狀態1表示已經安排好隊伍,0表示還沒,從小到大列舉狀態時候,一次性列舉三個是0的位置,考慮這三個人的最大權值,然後更新dp值。
然後可以稍微預處理一下,有效的狀態一定是1的數量是3的倍數的,可以把這些數提前存vector裡面,可以少列舉一些狀態,而且 \(C(n, 3)\)列舉時候可以先把狀態裡為0的位置存起來,再3個for迴圈列舉,也能最佳化一點點。
程式碼
點選檢視程式碼
#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = (1 << 22) + 100;
int a[maxn], b[30][30];
int dp[maxn];
int co[30][30][30];
vector<int> vec;
int cal(int x, int y, int z) {
int res = max({a[x], a[y], a[z], b[x][y], b[y][z], b[x][z],
b[x][y] * b[y][z] * b[x][z]});
return res;
}
signed main() {
int n;
cin >> n;
for (int i = 0; i < (1 << n); i++)
if (__builtin_popcount(i) % 3 == 0)
vec.push_back(i);
for (int i = 0; i < (1 << n); i++)
dp[i] = -1e18;
dp[0] = 0;
for (int i = 0; i < n; i++)
cin >> a[i];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> b[i][j];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
co[i][j][k] = cal(i, j, k);
for (auto u : vec) {
vector<int> tem;
for (int i = 0; i < n; i++)
if (!(u >> i & 1))
tem.push_back(i);
int sz = tem.size();
for (int i = 0; i < sz; i++)
for (int j = i + 1; j < sz; j++)
for (int k = j + 1; k < sz; k++) {
int nxt = u | (1 << tem[i]) | (1 << tem[j]) | (1 << tem[k]);
dp[nxt] = max(dp[nxt], dp[u] + co[tem[i]][tem[j]][tem[k]]);
}
}
cout << dp[(1 << n) - 1] << "\n";
return 0;
}
B.模擬、高精
題意
給定一個n \((n \leq 21)\) 位的二進位制數,輸出它和十進位制數21相乘後的結果的二進位制表示
分析
由於乘法不用管進位制,所以直接類似高精度乘法一樣,對這兩個二進位制數乘法,並且21比較小已經給定,甚至可以直接取21的二進位制為1的那些位,相當於把原來二進位制串右移對應位後相加,資料範圍比較小,所以隨便怎麼暴力都可以。
程式碼
點選檢視程式碼
#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 1e5 + 10;
int a[maxn], b[maxn], c[maxn];
signed main() {
string str;
cin >> str;
reverse(all(str));
for (int i = 0; i < str.length(); i++)
a[i] = str[i] - '0';
int tem = 21, cnt = 0;
while (tem) {
b[cnt++] = tem % 2;
tem /= 2;
}
for (int i = 0; i < 5; i++) {
if (b[i]) {
for (int j = 0; j < str.length(); j++) {
c[j + i] += a[j];
}
}
}
int mx = str.length() + 30;
for (int i = 0; i <= mx; i++) {
c[i + 1] += c[i] / 2;
c[i] %= 2;
}
string ans;
for (int i = 0; i <= mx + 31; i++)
ans.push_back(c[i] + '0');
while (!ans.empty() && ans.back() == '0')
ans.pop_back();
reverse(all(ans));
cout << ans << "\n";
return 0;
}
C. 01BFS
題意
有一個二維無限平面,給定起始點 \((x1, y1)\) 和終點 \((x2, y2)\), 平面上有 \(n\) 個障礙物,它們的座標都是 \(0 \leq x \leq 10^3\), \(0 \leq y \leq 10^3\),問從起點到終點,最少要穿過幾個障礙物
分析
這題應該有更簡單的方法,但是賽時比較緊張,第一反應是建一個最短路,相鄰格點建邊,目標有冰塊就權值1,否則權值0,然後跑迪傑斯特拉。不過發現評測機跑樣例都記憶體超限了,就被迫考慮最佳化,發現剛好權值都是0和1,想到01bfs,就不用建邊,直接跑最短路,在佇列取出來的時候再判斷相鄰節點有哪些,然後權值是1的放隊尾,權值0的放隊首。賽後想想,可能普通的最短路也行,只要不顯式的建邊,可能也不會爆記憶體。
這題有個坑點,就是建邊時候判斷邊界要是1001,不能只判斷到1000,因為雖然那些座標都在1000以內,但是實際人是可以繞到1000外面再走回去,所以得多往外面判斷幾格。
點選檢視程式碼
#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 1e3 + 10;
int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
int tag[maxn][maxn], dis[maxn * maxn], vis[maxn * maxn];
int trans(int i, int j) {
return (i - 1) * 1005+ j;
}
signed main() {
memset(dis, 0x3f, sizeof dis);
int n, x1, y1, x2, y2;
cin >> n >> x1 >> y1 >> x2 >> y2;
int m = 1e3 + 5;
for (int i = 1; i <= n; i++) {
int x, y;
cin >> x >> y;
tag[x][y] = 1;
}
deque<int> dq;
dis[trans(x1, y1)] = 0;
dq.push_back(trans(x1, y1));
while (!dq.empty()) {
int u = dq.front();
dq.pop_front();
if (vis[u]) continue;
vis[u] = 1;
int x = (u - 1) / m + 1, y = u % m;
if (x == x2 && y == y2) {
cout << dis[u] << "\n";
return 0;
}
for (int i = 0; i < 4; i++) {
int xx = x + dx[i], yy = y + dy[i];
if (xx < 1 || xx > m || yy < 1 || yy > m) continue;
int v = trans(xx, yy);
dis[v] = min(dis[v], dis[u] + tag[xx][yy]);
if (tag[xx][yy]) dq.push_back(v);
else dq.push_front(v);
}
}
return 0;
}
5.二分、倍增
這題我感覺是前六題裡面最難的,一開始還讀錯題了
題意
原題意有點繞,我直接給出形式化題意吧。給定一個陣列, 將其分成不超過 \(k\) 個連續子陣列, 每一段內的極差 (最大值減最小值)不超過 \(d\)。然後問三個問題:
假如只給定 \(k\) 求最小的 \(d\), 假如只給定 \(d\) 求最小的 \(k\), 假如給定了 \(d\) 和 \(k\) ,問最長能從原陣列裡取多長的子陣列。
分析
- 第一個問題,給定分段數,求最小極差,顯然可以二分答案,檢驗方式也很明顯,假設當前二分的答案是mid,從頭到尾遍歷陣列,維護當前分段的最大最小值,如果超了就另起一段。看最後分段的數量有沒有超過 \(k\)。
- 第二個問題,給定極差,求最小分段數。發現這不就是第一個問題裡的check函式,所以從頭掃一遍陣列,維護極差,超了另起一段,最後總的段數就是答案。
- 第三個問題比較難一點,給定 \(d\) 和 \(k\) ,求最長的連續子陣列滿足這兩個條件的長度。會發現,假如選定了這個子陣列的起始位置,然後就是之前的check函式那樣的暴力判斷,看什麼時候不滿足這兩個條件,複雜度 \(O(n^2)\)。然後想到,每個位置,往後一直到不合法為止,這個長度是固定的,相當於是一段一段的跳。所以可以預處理,從每個位置開始,往後第一個不合法的地方,列舉起點後就可以連著跳 \(k\) 次,看最後到了哪裡。然後發現這個過程可以用倍增維護,直接預處理每個位置往後跳 \(2^i\) 個段的位置,就可以列舉起點後 \(logk\)的判斷終點。然後怎麼判斷第一次跳到哪裡會超,可以用st表加二分,看第一次最大值和最小值差超過 \(d\) 的位置就是所求的,然後倍增處理查詢即可。
程式碼
點選檢視程式碼
#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define ll long long
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 5e5 + 10;
int arr[maxn], lg[maxn];
int mx[maxn][22], mn[maxn][22];
int st[maxn][22];
int n, k, d;
int qmn(int l, int r) {
int k = lg[r - l + 1];
return min(mn[l][k], mn[r - (1 << k) + 1][k]);
}
int qmx(int l, int r) {
int k = lg[r - l + 1];
return max(mx[l][k], mx[r - (1 << k) + 1][k]);
}
bool ck1(int mid) {
// 分成 k 段, 求最小 段內極差
int cnt = 1;
int mxx = arr[1], mnn = arr[1];
for (int i = 1; i <= n; i++) {
mxx = max(mxx, arr[i]);
mnn = min(mnn, arr[i]);
if (mxx - mnn > mid) {
cnt++;
mxx = arr[i];
mnn = arr[i];
}
if (cnt > k) return 0;
}
return cnt <= k;
}
signed main() {
for (int i = 2; i < maxn; i++)
lg[i] = lg[i >> 1] + 1;
cin >> n >> k >> d;
for (int i = 1; i <= n; i++)
cin >> arr[i], mn[i][0] = mx[i][0] = arr[i];
for (int j = 1; j < 21; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
mn[i][j] = min(mn[i][j - 1], mn[i + (1 << (j - 1))][j - 1]);
mx[i][j] = max(mx[i][j - 1], mx[i + (1 << (j - 1))][j - 1]);
}
{
int l = 0, r = 1e9, ans = 0;
while (l <= r) {
int mid = ((ll)l + r) >> 1;
if (ck1(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
cout << ans << "\n";
}
{
//給定極差 d 求最小段數
int mxx = arr[1], mnn = arr[1], ans = 1;
for (int i = 1; i <= n; i++) {
mxx = max(mxx, arr[i]);
mnn = min(mnn, arr[i]);
if (mxx - mnn > d) {
mxx = arr[i], mnn = arr[i];
ans++;
}
}
cout << ans << "\n";
}
{
// 預處理每個位置開始往後跳第一次越界的地方
for (int i = 1; i <= n; i++) {
int l = i + 1, r = n, ans = n + 1;
while (l <= r) {
int mid = ((ll)l + r) >> 1;
int mxx = qmx(i, mid), mnn = qmn(i, mid);
if (mxx - mnn > d) ans = mid, r = mid - 1;
else l = mid + 1;
}
st[i][0] = ans;
}
st[n + 1][0] = n + 1;
for (int j = 1; j < 21; j++)
for (int i = 1; i <= n + 1; i++)
st[i][j] = st[st[i][j - 1]][j - 1];
int ans = 0;
for (int i = 1; i <= n; i++) { //列舉起點
int s = i;
for (int j = 0; j < 21; j++)
if (k >> j & 1)
s = st[s][j];
ans = max(ans, s - i);
if (n - i + 1 <= ans)
break;
}
cout << ans << "\n";
}
return 0;
}
6. DP,LIS
題意
給定長度為 \(n\) (\(n\)) 的陣列,問有多少個子序列是嚴格山峰的(有且僅有一個最大值,左邊嚴格遞增,右邊是嚴格遞減)
分析
考慮去列舉山峰的最大值,以它為最大值的貢獻,是左側的嚴格上升子序列,並且結尾小於當前最大值。右側的嚴格下降子序列,並且開頭小於中間這個最大值的總和,二者乘積即為當前的貢獻。所以就去處理左右兩側,然後就轉化為,對每個 \(i\) 求以它結尾的最長上升子序列個數,用樹狀陣列即可處理,是經典dp。再倒著做一遍,然後前字尾兩個樹狀陣列,不斷維護一下前字尾的那些子陣列數量就好。
程式碼
點選檢視程式碼
#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 3e5 + 10;
struct BIT {
vector<int> v;
int len;
int lowbit(int x) { return x & -x; }
BIT(int n) {
len = n + 3;
v.resize(len);
}
void update(int i, int x) {
i++;
for (int pos = i; pos <= len; pos += lowbit(pos))
v[pos] = ((v[pos] + x) % mod + mod) % mod;
}
int ask(int i) {
int res = 0;
i++;
for (int pos = i; pos; pos -= lowbit(pos))
res = ((res + v[pos]) % mod + mod) % mod;
return res;
}
void clear() {
for (int i = 0; i < len; i++)
v[i] = 0;
}
};
int arr[maxn], b[maxn];
int predp[maxn], sufdp[maxn];
signed main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> arr[i];
b[i] = arr[i];
}
sort(b + 1, b + 1 + n);
int len = unique(b + 1, b + 1 + n) - b - 1;
for (int i = 1; i <= n; i++) {
arr[i] = lower_bound(b + 1, b + 1 + len, arr[i]) - b;
}
BIT pre(len), suf(len);
pre.update(0, 1);
for (int i = 1; i <= n; i++) {
predp[i] = pre.ask(arr[i] - 1);
pre.update(arr[i], predp[i]);
}
suf.update(0, 1);
for (int i = n; i >= 1; i--) {
sufdp[i] = suf.ask(arr[i] - 1);
suf.update(arr[i], sufdp[i]);
}
pre.clear();
pre.update(0, 1);
int ans = 0;
for (int i = 1; i <= n; i++) {
suf.update(arr[i], -sufdp[i]);
ans = ((ans + pre.ask(arr[i] - 1) * suf.ask(arr[i] - 1) % mod) % mod + mod) % mod;
pre.update(arr[i], predp[i]);
}
cout << ans << "\n";
return 0;
}
點選檢視程式碼
#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 6e5 + 10;
vector<pii> G[maxn];
int lg[maxn], fa[maxn][23], dep[maxn];
int xu[maxn], val[maxn], sub[maxn];
struct Node {
int a, b;
Node(int a1, int b1) {a = a1, b = b1;}
};
void dfs(int u, int f, int deep) {
fa[u][0] = f, dep[u] = deep;
for (int i = 1; i <= lg[dep[u]]; i++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (auto [v, w] : G[u]) {
if (v == f) continue;
val[v] = w;
dfs(v, u, deep + 1);
}
}
int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
while (dep[u] > dep[v])
u = fa[u][lg[dep[u] - dep[v]]];
if (u == v) return u;
for (int i = lg[dep[u]]; ~i; i--)
if (fa[u][i] != fa[v][i])
u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
void dfs1(int u, int f) {
for (auto[v, w] : G[u]) {
if (v == f) continue;
dfs1(v, u);
sub[u] += sub[v];
}
}
bool operator < (Node a, Node b) {
auto [x, y] = a;
int res1 = x * y - (x / 2) * y;
auto [c, d] = b;
int res2 = c * d - (c / 2) * d;
return res1 < res2;
}
signed main() {
for (int i = 2; i < maxn; i++)
lg[i] = lg[i >> 1] + 1;
int n, m, k;
cin >> n >> m >> k;
for (int i = 1; i < n; i++) {
int u, v, w;
cin >> u >> v >> w;
G[u].emplace_back(v, w);
G[v].emplace_back(u, w);
}
for (int i = 1; i <= m; i++)
cin >> xu[i];
xu[0] = 1;
dfs(1, 0, 0);
for (int i = 1; i <= m; i++) {
int u = xu[i - 1], v = xu[i];
sub[u]++, sub[v]++;
sub[lca(u, v)] -= 2;
}
dfs1(1, 0);
priority_queue<Node> q;
for (int i = 2; i <= n; i++) {
q.push(Node(val[i], sub[i]));
}
while (k-- && !q.empty()) {
auto [w, cnt] = q.top();
q.pop();
w /= 2;
if (cnt)
q.push(Node(w, cnt));
}
int ans = 0;
while (!q.empty()) {
auto [w, cnt] = q.top();
q.pop();
ans += cnt * w;
}
cout << ans << "\n";
return 0;
}
後記
第四題不會做可惜了,這裡放個題意,感興趣的可以想想。給一個長度為 \(n\) 的環形字串,每個位置有一個權值。然後多次操作,第一種操作是修改單點權值。第二種操作是選擇一個區間,然後從左到右開始,每有兩個同樣的字元,就刪去這兩個字元以及中間的字元。求最後剩下的字元的位置上的權值和。
再練一年希望明年拿個金,加訓!