2020, XIII Samara Regional Intercollegiate Programming Contest 題解
A. Array's Hash
題意:\(Vasya\) 發明了一種新的散列函數,給定一個數組 \(a_n\) ,這個散列函數每次將數組中的前兩個元素替換為一個值為 \(a_2 - a_1\) 的元素,直到只剩下一個元素,得到的這個元素即為新的哈希值。現在有 \(q\) 次詢問,每次詢問前該數組都先執行一個區間加操作,然后詢問該數組的哈希值。
分析:這個新的哈希值就是 \(a_n-a_{n-1}+a_{n-2}-a_{n-3}+a_{n-4}\cdots\) ,因此我們直接將奇偶下標各看作一個數組進行操作即可。實際上可以 \(O(n)\) 做,不過這里偷懶直接復制了個樹狀數組板子。
#include <bits/stdc++.h>
#define int long long
#define SIZE 1000010
using namespace std;
int n;
namespace FenwickTree {
int ta1[SIZE], ta2[SIZE], tb1[SIZE], tb2[SIZE];
int lowbit(int x) { return x & (-x); }
void add(int pos, int x, int t[]) { // 單點修改
for (; pos <= n; pos += lowbit(pos)) {
t[pos] += x;
}
}
void add(int l, int r, int x, int t1[], int t2[]) { // [l, r] 區間+x
add(l, x, t1);
add(r + 1, -x, t1);
add(l, l * x, t2);
add(r + 1, -x * (r + 1), t2);
}
int query_presum(int pos, int t[]) { // 單點查詢,即對差分數組求前綴和
int ans = 0;
for (; pos > 0; pos -= lowbit(pos)) {
ans += t[pos];
}
return ans;
}
int query_sum(int l, int r, int t1[], int t2[]) { // 區間修改下的區間查詢
int p1 = l * query_presum(l - 1, t1) - query_presum(l - 1, t2);
int p2 = (r + 1) * query_presum(r, t1) - query_presum(r, t2);
return p2 - p1;
}
}
using namespace FenwickTree;
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; ++i) {
int x; cin >> x;
int pos = (i + 1) >> 1;
if (i & 1) add(pos, pos, x, ta1, ta2);
else add(pos, pos, x, tb1, tb2);
}
int q; cin >> q;
for (int i = 1; i <= q; ++i) {
int l, r, v;
cin >> l >> r >> v;
int la = ((l + 1) >> 1) + !(l & 1), ra = (r + 1) >> 1;
int lb = (l >> 1) + (l & 1), rb = r >> 1;
if (la <= ra) add(la, ra, v, ta1, ta2);
if (lb <= rb) add(lb, rb, v, tb1, tb2);
int ans = query_sum(1, n, ta1, ta2) - query_sum(1, n, tb1, tb2);
if (n & 1) cout << ans << '\n';
else cout << -ans << '\n';
}
}
B. Bonuses on a Line
題意:數軸上有 \(n\) 個點,坐標為 \((x_i,0)\) ,你從原點開始能夠移動 \(t\) 個單位距離,詢問最多能夠經過幾個點。
分析:將正點和負點分開存,然后枚舉我們到達的左端點(右端點同理),二分搜索從該端點向右走最多能夠經過幾個正點。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
ll n, t; cin >> n >> t;
vector<ll> pos, neg;
for (int i = 0; i < n; ++i) {
ll x; cin >> x;
if (x < 0) neg.emplace_back(-x);
else pos.emplace_back(x);
}
sort(neg.begin(), neg.end());
sort(pos.begin(), pos.end());
ll ans = 0;
for (int i = 0; i < neg.size(); ++i) {
if (neg[i] > t) break;
ll maxd = t - neg[i] - neg[i];
ll res = 0;
if (maxd > 0) res = upper_bound(pos.begin(), pos.end(), maxd) - pos.begin();
ans = max(ans, i + res + 1);
}
for (int i = 0; i < pos.size(); ++i) {
if (pos[i] > t) break;
ll maxd = t - pos[i] - pos[i];
ll res = 0;
if (maxd > 0) res = upper_bound(neg.begin(), neg.end(), maxd) - neg.begin();
ans = max(ans, i + res + 1);
}
cout << ans;
}
C. Manhattan Distance
題意:給定二維平面上 \(n\) 個點,求曼哈頓距離第 \(k\) 近的點對。
分析:二分搜索答案,計數時有一個技巧:因為距離某個點 \((x,y)\) 的曼哈頓距離不超過 \(d\) 的點可以看作在一個旋轉了 \(90°\) 的正方形中,該正方形頂點分別為 \((x-d,y);(x,y-d);(x+d,y);(x,y+d)\) ,處理較為復雜;因此,我們不妨直接將坐標系旋轉 \(90°\) ,那樣所有距離某個點 \((x,y)\) 的曼哈頓距離不超過 \(d\) 的點都可以看作在一個頂點分別為 \((x-\frac{d}{2},y-\frac{d}{2});(x-\frac{d}{2},y+\frac{d}{2});(x+\frac{d}{2},y+\frac{d}{2});(x+\frac{d}{2},y-\frac{d}{2})\) 的正方形中,我們可以利用樹狀數組快速計數。時間復雜度 \(O(n\log n \log 4e8)\) 。
#include <bits/stdc++.h>
#include <random>
#define ll long long
#define mp make_pair
#define SIZE 100010
using namespace std;
ll n, k;
namespace FenwickTree {
int t[SIZE];
int lowbit(int x) { return x & (-x); }
void add(int pos, int x, int t[]) { // 單點修改
for (; pos <= n; pos += lowbit(pos)) {
t[pos] += x;
}
}
int query_presum(int pos, int t[]) { // 單點查詢
int ans = 0;
for (; pos > 0; pos -= lowbit(pos)) {
ans += t[pos];
}
return ans;
}
}
using namespace FenwickTree;
ll check(int d, vector<pair<int, int> >& p, vector<int>& y) {
ll res = 0;
memset(t, 0, sizeof(t));
for (int i = 0, j = 0; i < n; ++i) {
while (j < i && p[i].first - p[j].first > d) {
add(lower_bound(y.begin(), y.end(), p[j].second) - y.begin() + 1, -1, t);
j++;
}
res += query_presum(upper_bound(y.begin(), y.end(), p[i].second + d) - y.begin(), t);
res -= query_presum(lower_bound(y.begin(), y.end(), p[i].second - d) - y.begin(), t);
add(lower_bound(y.begin(), y.end(), p[i].second) - y.begin() + 1, 1, t);
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
cin >> n >> k;
vector<pair<int, int> > p(n);
vector<int> y;
for (int i = 0; i < n; ++i) {
int a, b; cin >> a >> b;
p[i].first = a + b;
p[i].second = a - b;
y.emplace_back(p[i].second);
}
sort(p.begin(), p.end());
sort(y.begin(), y.end());
y.erase(unique(y.begin(), y.end()), y.end());
int L = 0, R = 4e8, mid;
while (R > L) {
mid = (L + R) >> 1;
if (check(mid, p, y) < k) L = mid + 1;
else R = mid;
}
cout << L;
}
D. Lexicographically Minimal Shortest Path
題意:給定一個 \(n\) 個點 \(m\) 條邊的無向無權圖,每條邊都有一個字母,要求找出該圖中從節點 \(1\) 到節點 \(n\) 的字典序最小的最短路。
分析:我們先將所有點距離點 \(n\) 的距離預處理出來,由於無權邊,因此一個 \(O(m)\) 的 \(BFS\) 就能解決。隨后,從節點 \(1\) 出發,搜索所有后繼節點,並找到所有后繼節點中距離點 \(n\) 的距離為前驅節點到點 \(n\) 距離減一的節點中字典序最小的那個(如果有多個就全部丟進 \(vector\) ),然后重復這一操作直到找到字典序最小的最短路。
#include <bits/stdc++.h>
#define ll long long
#define mp make_pair
#define SIZE 200010
using namespace std;
vector<pair<int, char> > vec[SIZE];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int n, m; cin >> n >> m;
for (int i = 0; i < m; ++i) {
int a, b; char c;
cin >> a >> b >> c;
--a, --b;
vec[a].emplace_back(mp(b, c));
vec[b].emplace_back(mp(a, c));
}
vector<int> dis(n, -1);
queue<int> q;
q.push(n - 1);
dis[n - 1] = 0;
while (!q.empty()) {
int top = q.front();
q.pop();
for (auto i : vec[top]) {
int to = i.first;
if (dis[to] == -1) {
dis[to] = dis[top] + 1;
q.push(to);
}
}
}
string ans = "";
vector<int> pa(n, -1);
vector<int> tmp;
tmp.emplace_back(0);
for (int i = 0; i < dis[0]; ++i) {
char minc = 'z' + 1;
vector<int> nxt;
for (auto j : tmp) {
for (auto k : vec[j]) {
if (dis[k.first] == dis[j] - 1) {
if (k.second < minc) {
pa[k.first] = j;
minc = k.second;
nxt = { k.first };
}
else if (k.second == minc) {
pa[k.first] = j;
nxt.emplace_back(k.first);
}
}
}
}
sort(nxt.begin(), nxt.end());
nxt.erase(unique(nxt.begin(), nxt.end()), nxt.end());
tmp = nxt;
ans += minc;
}
vector<int> path;
int fa = n - 1;
while (fa != -1) {
path.emplace_back(fa + 1);
fa = pa[fa];
}
reverse(path.begin(), path.end());
cout << path.size() - 1 << '\n';
for (auto i : path) cout << i << ' ';
cout << '\n' << ans;
}
E. Fluctuations of Mana
題意:略。
分析:前綴和求最值。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
ll n, ans = 1e18;
cin >> n;
vector<ll> a(n);
vector<ll> pre(n);
for (auto& i : a) cin >> i;
for (int i = 0; i < n; ++i) pre[i] = (!i ? a[i] : a[i] + pre[i - 1]);
for (auto i : pre) ans = min(ans, i);
cout << (ans >= 0 ? 0 : -ans);
}
F. Moving Target
題意:有一排編號分別為 \(1,2,\cdots,n\) 的窗戶,某個窗戶后面有一個目標物,你每次可以打開一個窗戶來尋找這個目標物,如果找到就結束了,如果沒找到就要繼續找並且該目標物會轉移到右邊的窗戶(即從 \(k\) 變成 \(k+1\) ),請你給出一種尋找次數最少的構造,必須保證一定能夠找到這個目標物。
分析:其實就是構造一個 1 3 5 ...
的數列,注意特判奇偶。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int n; cin >> n;
cout << ((n & 1) ? (n + 1) / 2 : n / 2 + 1);
cout << '\n';
for (int i = 0; i < (n + 1) / 2; ++i) cout << 2 * i + 1 << ' ';
if (!(n & 1)) cout << n;
}
G. Nuts and Bolts
題意:交互題。有 \(n\) 個尺寸為 \(1,2,\cdots,n\) 的螺栓和螺母,現在他們的順序被打亂了,你需要將他們兩兩配對(大小相同的),最多能夠詢問 \(5n\log n\) 次。每次詢問中,你可以詢問兩個下標 \(i\) 和 \(j\) 表示詢問第 \(i\) 個螺栓和第 \(j\) 個螺母的大小關系,如果螺栓的尺寸大於螺母將返回 \(>\) ,若等於返回 \(=\) ,若小於返回 \(<\) 。
分析:肯定是分治的思想,我們每次隨機一個下標 \(x\) ,通過兩次 \(O(n)\) 的詢問將數組 \(a\) 中小於 \(a[x]\) 的元素和大於 \(a[x]\) 的元素分別丟進兩個數組,將數組 \(b\) 中小於 \(a[x]\) 的元素和大於 \(a[x]\) 的元素分別丟進兩個數組,並且找到數組 \(b\) 中與 \(a[x]\) 相等的元素的下標。然后將小於 \(a[x]\) 的兩個數組和大於 \(a[x]\) 的兩個數組分成兩組進行遞歸搜索,理論復雜度 \(O(n\log n)\) 。
#include <bits/stdc++.h>
#include <random>
using namespace std;
vector<int> ans;
void solve(vector<int>& A, vector<int>& B) {
if (A.empty()) return;
default_random_engine seed(time(0));
uniform_int_distribution<int> gen(0, A.size() - 1);
int x = A[gen(seed)];
int pos = -1;
vector<int> AL, AR, BL, BR;
for (auto i : B) {
cout << "? " << x + 1 << " " << i + 1 << endl;
char ch; cin >> ch;
if (ch == '<') BR.emplace_back(i);
else if (ch == '>') BL.emplace_back(i);
else pos = i;
}
ans[x + 1] = pos + 1;
for (auto i : A) {
cout << "? " << i + 1 << " " << pos + 1 << endl;
char ch; cin >> ch;
if (ch == '<') AL.emplace_back(i);
else if (ch == '>') AR.emplace_back(i);
}
solve(AL, BL);
solve(AR, BR);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int n; cin >> n;
ans.resize(n + 1, -1);
vector<int> A(n), B(n);
for (int i = 0; i < n; ++i) A[i] = B[i] = i;
solve(A, B);
cout << "!";
for (int i = 1; i <= n; ++i) cout << " " << ans[i];
}
H. Tree Painting
題意:給定一棵 \(n\) 個點的樹,現在每次操作能夠選擇樹上的兩個節點,然后將這兩個節點之間的邊和點全部染色,詢問將整棵樹的邊和點完全染色需要幾次操作。
分析:最優策略必定是每次選擇兩個葉子節點,然后將這兩個節點間的路徑染色,因此結果為 \(\frac{葉子節點數+1}{2}\) 。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int n; cin >> n;
vector<int> deg(n + 1);
for (int i = 1; i < n; ++i) {
int u, v; cin >> u >> v;
deg[u]++; deg[v]++;
}
int leaves = 0;
for (int i = 1; i <= n; ++i)
if (deg[i] == 1)
++leaves;
cout << (leaves + 1) / 2;
}
I. Sorting Colored Array
題意:給定一個數組,第 \(i\) 個位置的元素有一個數值 \(a_i\) 和一個顏色 \(c_i\) ,不同顏色的相鄰元素可以交換位置,詢問能否將數組交換為按照數值大小從小到大排列的。
分析:因為相同顏色的元素不能交換,因此我們將元素按照顏色分類存儲,如果某種顏色的顏色中存在逆序對,那么就不能,否則就能。
#include <bits/stdc++.h>
#define SIZE 1000010
using namespace std;
bool check(vector<int>& a, vector<int>& b) {
for (int i = 0; i < a.size(); ++i)
if (a[i] != b[i])
return true;
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int n; cin >> n;
map<int, vector<int> > MP;
for (int i = 1; i <= n; ++i) {
int x, y; cin >> x >> y;
MP[y].emplace_back(x);
}
for (auto it : MP) {
vector<int> a, b;
a = b = it.second;
sort(a.begin(), a.end());
if (check(a, b)) {
cout << "NO";
return 0;
}
}
cout << "YES";
}
J. The Battle of Mages
題意:輸出答案題。有兩個能隨機召喚生物的法師,每個召喚出的生物有一個屬性:攻擊力。每個法師能從他的召喚池中召喚 \(k\) 只不同的生物,召喚出的生物攻擊力之和更高的獲勝。現在要求你構造出這兩個法師的召喚池,使得 \(k=1,3\) 時,第一個法師勝率更高, \(k=2\) 時第二個法師勝率更高。
分析:略。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
cout << "3\n2 7 7\n3\n5 5 5";
}
K. Table
題意:給定一個桌子的四個桌腳的長度,判斷這四個桌腳能否恰好保證支撐起這個桌子(即保證桌面是一個平面)。
分析:如圖:
設這四根桌腳的長度分別為 \(a_0,a_1,a_2,a_3\) (從小到大排列),如果 \(ABCD\) 為一平面,則必須滿足:
\(|DM|=|CN|\Leftrightarrow |CC'|=|CN|+|C'N|=|BB'|+|DM|\Leftrightarrow a_3=a_1+a_2-a_0\)
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
vector<int> a(4);
for (auto& i : a) cin >> i;
sort(a.begin(), a.end());
if (a[3] == a[2] + a[1] - a[0]) cout << "YES";
else cout << "NO";
}
L. The Dragon Land
題意:一個勇者需要穿越一片龍的領地,一共有 \(n\) 只龍,每只龍有一個賞金 \(a_i\) ,勇者每攻擊一只龍都需要花錢修一次裝備,第一次修花費 \(1\) ,每次修復的價格增加 \(1\) ,詢問勇者最多能賺多少錢。
分析:排序一下貪心。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
ll n; cin >> n;
vector<ll> a(n);
for (auto& i : a) cin >> i;
sort(a.begin(), a.end());
reverse(a.begin(), a.end());
ll ans = 0, cost = 1;
for (auto i : a) {
if (i > cost) {
ans += i - cost;
++cost;
}
}
cout << ans;
}
M. Notifications
題意:略。
分析:簡單模擬。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int t; cin >> t;
vector<pair<ll, ll> > a(t);
for (auto& i : a) cin >> i.first >> i.second;
sort(a.begin(), a.end());
ll ans = a[0].first + a[0].second;
for (int i = 1; i < t; ++i) {
if (a[i].first <= ans) ans += a[i].second;
else ans = a[i].first + a[i].second;
}
cout << ans;
}