- Preface
- Problem I. 迴圈蘋果串
- Problem K. 派對遊戲
- Problem B. 無數的我
- Problem F. 訂製服裝
- Problem E. 迴旋鏢
- Problem D. 國際大胃王錦標賽
- Problem M. 合併
- Problem C. 書包與最長上升子序列
- PostScript
Preface
這場比賽是在今年上半年 vp 過一次的,當時是過了五個題,近兩天打算重新拿出來單挑一把,結果成功的在前面的簽到題卡住了,搞了老半天,最後勉勉強強還是五題,罰時直接吃屎了。
所有程式碼前面的火車頭
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
#include <set>
#include <queue>
#include <map>
#include <iomanip>
#define endl '\n'
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define rep2(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
template<typename T>
void cc(vector<T> tem) { for (auto x : tem) cout << x << ' '; cout << endl; }
void cc(int a) { cout << a << endl; }
void cc(int a, int b) { cout << a << ' ' << b << endl; }
void cc(int a, int b, int c) { cout << a << ' ' << b << ' ' << c << endl; }
void fileRead() { freopen("D:\\AADVISE\\cppvscode\\CODE\\in。txt", "r", stdin); }
void kuaidu() { ios::sync_with_stdio(false), cin。tie(0), cout。tie(0); }
inline int max(int a, int b) { if (a < b) return b; return a; }
inline int min(int a, int b) { if (a < b) return a; return b; }
void cmax(int& a, const int b) { if (b > a) a = b; }
void cmin(int& a, const int b) { if (b < a) a = b; }
using PII = pair<int, int>;
using i128 = __int128;
//--------------------------------------------------------------------------------
const int N = 1e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
//--------------------------------------------------------------------------------
Problem I. 迴圈蘋果串
題意就是能夠任意挪動子串任意次,讓原本的字串(只有 0,1)挪動之後變成有序的,求出來最少的挪動次數。
例如 00110101,我們顯而易見肯定是每一次都把隔開的 1 給合併到一起,這樣操作的次數最後一定會是最少的。
所以我們只需要找到 1 的聯通塊個數就好了。
有個小細節就是如果字串的末尾是 1,那這個聯通塊就不算入計數。
signed main() {
kuaidu();
T = 1;
//cin >> T;
while (T--) {
string s; cin >> s;
s = '0' + s;
s = s + '0';
int len = s。size();
int cnt = 0;
rep(i, 1, len - 1) {
if (s[i] != s[i - 1] and s[i] == '0' and s[i - 1] == '1')
cnt++;
}
if (s[len - 2] == '1')
cout << cnt - 1 << endl;
else cout << cnt << endl;
}
return 0;
}
/*
*/
Problem K. 派對遊戲
題意大致如下:
有 n 個整數 1, 2, 3, ... , n 從左到右順序排成一行,某個我和萍琪派將依次嘗試進行如下操作:
• 若剩下的整數的異或和不為 0,移走這一行整數中最左邊的數或最右邊的數,並不改變其餘數字的
順序。若當前行動者無法操作,那麼其輸掉遊戲。
對於這種,直接打表即可,(以下用 0 和 1 代表自己是輸,贏)透過打表後我們發現,
n=1,2,3,4,5,6,7,8 時,答案依次是 1,0,0,1,1,0,0,1,1...
除去第一個是 1 之外是一個 0,0,1,1 的迴圈節,根據這個直接輸出就好了。
signed main() {
kuaidu();
T = 1;
cin >> T;
while (T--) {
cin >> n;
if (n == 1) {
cout << "Fluttershy" << endl;
continue;
}
n -= 1;
n %= 4;
if (n == 2 or n == 1) cout << "Pinkie Pie" << endl;
else cout << "Fluttershy" << endl;
}
return 0;
}
/*
*/
Problem B. 無數的我
稍微吃屎的一集,一開始想的直接取最小和最大的執行 n 次操作,並且非常弱智的直接交了一發 WA 了,之後改變做法,覺得這種沒有任何道理的絕對不對,之後想了想,操作應該是可以轉化成總和 sum 一定,去分配給 n 個數字。所以我們就直接二進位制從高位到低位列舉 i,當這些 n 個數字的第 i 位全是 0 的時候,後面全是 1 的和是大於 sum,則說明可以在當前 i 位都不放 1,否則就說明當前這一位必須要放 1,既然放了 1,我們就不妨 i 位能放多少 1 就放多少 1,按照這個思路貪下去就好了。
int kuai(int a, int b) {
int l = 1;
while (b) { if (b % 2)l = l * a; a = a * a; b /= 2; }
return l;
}
signed main() {
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> n;
int sum = 0;
rep(i, 1, n) {
int a; cin >> a; sum += a;
}
int ans = 0;
rep2(i, 32, 0) {
if (i == 0) {
if (sum) ans |= 1ll;
break;
}
if (sum > kuai(2, i) * n - n) {
//如果大於,則說明當前這一位必須要有1,就儘量取滿,sum / kuai(2, i)和 n要取一個min
sum -= min(sum / kuai(2, i), n) * kuai(2, i);
ans |= 1ll << i;
}
if (!sum) break;
}
cout << ans << endl;
}
return 0;
}
/*
*/
Problem F. 訂製服裝
最吃屎的地方來了,這個題當時是隊友寫的,自己沒太怎麼思考,當時直接去開 E 了,結果自己現在被這題差點直接卡死。
首先我們可以在 log(n*n)時間內算出來某個點是誰,這個是 20 次。
當時寫了一個巨假的做法,是去找到左下角的點是誰,然後逐漸往右上走,這樣可以劃分一半的區域是大於還是小於,剩下的一半區域依舊這樣做,這樣算下來大概是 log(n*n)log(n*n)2*n ,(這裡還算錯了,實際是 8e5,算成了 8e4)所以開始賭徒模式了。但是在實現的時候由於我依賴於左下角點的權值,導致往右上走實現不太科學,再加上次數可能會超,所以卡死了。
下面是正解,我們應該二分權值,然後在這個地圖上找有多少個大於他(小於他),這樣子時間複雜度還會少了一個 log,這樣才是正解。
//找x,y這個點和val的關係,最後返回的是個數
int dfs(int x, int y, int val) {
if (x <= 0 || y > n) return 0;
cout << "? " << x << " " << y << " " << val << endl; cout。flush();
int t; cin >> t;
if (t == 1) {
return x + dfs(x, y + 1, val);
}
return dfs(x - 1, y, val);
}
int check(int val) {
int tem = dfs(n, 1, val);
return tem;
}
signed main() {
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> n >> k;
k = n * n - k + 1;
int l = 0, r = n * n + 1;
while (l + 1 != r) {
int mid = l + r >> 1;
if (check(mid) < k) l = mid;
else r = mid;
}
cout << "! " << r << endl; cout。flush();
}
return 0;
}
/*
*/
Problem E. 迴旋鏢
這個題比較的有意思,首先我們能想到這個題一定和直徑有關,我們假設如果$ k$ 是$ 1$ 的時候,那最後一定是選擇樹上的直徑中心,當$ k$ 稍微大的時候,那比較傾向選擇 $t0 \(時間生成的樹的直徑中心,不難想到找的直徑和時間是有關係的,而且大概是滿足某種單調性,當\) k\(逐漸大的時候,\)t $會逐漸變小。
之後可以想出來,我們可以列舉$ k$ 從$ n$ 到$ 1\(,\)t \(當前是\) t0$,當前的樹是 $t0 \(時刻的樹。如果當前的\) k$ 選擇當前樹的直徑中心在(t-t0)的時間可以覆蓋掉,那 \(ans[k]=t,k--。\)
直到無法覆蓋的時候,那麼 \(t++\),樹的直徑(可能)變化,再判斷能不能覆蓋。
中間有一個 \(trick\) 是關於直徑的更新,\(t++\)之後,我們會多更新一層節點,仔細想想會發現,在一棵樹上如果多了一個點 \(x\),對於直徑的影響就是 $x $對直徑的兩個端點的距離可能會比直徑大,所以我們只需要維護直徑的兩個端點就好了。
下面是 AC 程式碼加上少許註釋
//--------------------------------------------------------------------------------
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
vector<int> A[N];
int r, t0;
vector<int> ceng[N];
int dep[N];
int fa[N][27];
int ans[N];
//--------------------------------------------------------------------------------
void dfs(int x, int pa) {
dep[x] = dep[pa] + 1;
ceng[dep[x]]。push_back(x);
fa[x][0] = pa;
rep(i, 1, 20) fa[x][i] = fa[fa[x][i - 1]][i - 1];
for (auto y : A[x]) {
if (y == pa) continue;
dfs(y, x);
}
}
//求x和y點的lca
int lca(int x, int y) {
if (x == y) return x;
if (dep[x] < dep[y]) swap(x, y);
rep2(i, 20, 0) {
if (dep[fa[x][i]] < dep[y]) continue;
x = fa[x][i];
}
if (x == y) return x;
rep2(i, 20, 0) {
if (fa[x][i] == fa[y][i]) continue;
x = fa[x][i], y = fa[y][i];
}
return fa[x][0];
}
//求兩點之間距離
int dis(int x, int y) {
int t = lca(x, y);
return dep[x] + dep[y] - dep[t] - dep[t];
}
signed main() {
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> n;
//存圖
rep(i, 1, n - 1) {
int a, b; cin >> a >> b;
A[a]。push_back(b);
A[b]。push_back(a);
}
cin >> r >> t0;
//一開始先選擇r去預處理每一層的節點,方便下面更新
dfs(r, 0);
//q1和q2是直徑端點
int q1 = r, q2 = r;
rep(i, 1, t0 + 1) {
if (i + 1 <= n)
for (auto x : ceng[i + 1]) {
if (dis(x, q1) > dis(q1, q2)) {
q2 = x;
}
if (dis(x, q2) > dis(q1, q2)) {
q1 = x;
}
}
}
// cout << lca(6, 7) << endl;
// cout << dis(6, 7) << endl;
// cc(dep[6], dep[7], dep[2]);
int ned = 1;//ned是需要的時間
rep2(i, n, 1) {//i是列舉的k
if (i * ned >= (dis(q1, q2) + 1) / 2) {
ans[i] = ned;
continue;
}
while (i * ned < (dis(q1, q2) + 1) / 2) {
ned++;
if (t0 + 1 + ned <= n)
for (auto x : ceng[t0 + 1 + ned]) {
if (dis(x, q1) > dis(q1, q2)) {
q2 = x;
}
if (dis(x, q2) > dis(q1, q2)) {
q1 = x;
}
}
}
ans[i] = ned;//ans是比t0多的時間,最後還會再加上t0
}
rep(i, 1, n) {
cout << ans[i] + t0 << " ";
}
cout << endl;
}
return 0;
}
/*
8
1 2
1 4
1 5
3 6
2 3
4 7
7 8
2 1
*/
Problem D. 國際大胃王錦標賽
這個題說難不難,但說簡單也不簡單,我沒做出來
首先根據題目的問法,我們顯然是需要有一個 n 方的做法可以直接把 F 陣列求出來。但是如果單純的去想 dp 式子個人感覺不是太好想出來。這個題最後是透過前字尾最佳化做出來的。
我們顯然想到最優的做法一定是要麼不拐彎,要麼就拐一次彎。
我們先設 g[s][t]陣列是在 s 座標走不超過 t 秒的最大值(不拐彎)
F[s][t]是由以下 g 陣列推出來的
max{g[s][t],g[s-1][t-1],g[s-2][t-2],g[s-3][t-3],。。。,g[s+1][t-1],g[s+2][t-2],。。。}
因此 F[s][t]可以直接由 F[s-1][t-1]和 g[s][t]取 max
同時再倒著再來一遍就好了。
//--------------------------------------------------------------------------------
const int N = 5e3 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
int A[N];
int g[N][N + N];
int sum[N];
int dp[N][N + N];
//--------------------------------------------------------------------------------
// 1 2 3 4 5 6 7 8 9
signed main() {
// freopen("D:\\AADVISE\\cppvscode\\CODE\\in。txt", "r", stdin);
// freopen("D:\\AADVISE\\cppvscode\\CODE\\out。txt", "w", stdout);
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> n;
rep(i, 1, n) {
cin >> A[i];
sum[i] = sum[i - 1] + A[i];
}
rep(i, 1, n) {
rep(j, 1, n + n) {
int l = max(1ll, i - j);
int r = min(n, i + j);
g[i][j] = max(sum[i] - sum[l - 1], sum[r] - sum[i - 1]);
}
}
rep(i, 1, n) {
rep(j, 1, n + n) {
cmax(dp[i][j], dp[i - 1][j - 1]);
// cmax(dp[i][j], dp[i][j - 1]);
cmax(dp[i][j], g[i][j]);
// cmax(dp[i][j], suf[i][j]);
// cmax(dp[i][j], pre[i][j]);
}
}
rep2(i, n, 1) {
rep(j, 1, n + n) {
cmax(dp[i][j], dp[i + 1][j - 1]);
cmax(dp[i][j], g[i][j]);
}
}
int ans = 0;
rep(i, 1, n) {
int tem = 0;
rep(j, 1, n + n) {
tem ^= (j * dp[i][j]);
}
ans ^= (i + tem);
}
cc(ans);
}
return 0;
}
/*
6
7 2 1 3 0 8
*/
Problem M. 合併
這個題其實想到了一點就比較的 ez 了,首先能夠感覺到的是,合併的次數絕對不會很多,其實後來我算是誤打誤撞想到的思路,當時在發呆想如果合併的數字需要差值是 2 會怎麼樣,然後就發現這樣的話奇數就都沒有用了。然後就莫名其妙聯想到差值為 1 的話,那麼合併出來的新數將會是奇數。於是我們的思路便大差不差了。
可以去找當前序列中最大的偶數 x,找 x+1 存不存在,x-1 存不存在,存在就可以合併,不存在就找下一個偶數接著進行這樣的操作。
找 x+1 存不存在,因為 x+1 是奇數,所以他是可以合併操作得到的,去找尋 x/2 和 x/2+1,這樣子是 log 級別的。
其實思路不算太難,但是程式碼中間有小細節問題需要處理好才可以。時間複雜度上,會有 log(1e18)*n,在加上map會再多出來一個log,會爆掉,得卡常。 我們直接先找 x/2 存不存在,因為他一定會是偶數,所以我們直接 o1 判斷出來,如果他不存在的話就直接 return false 了,這樣說不定就不用找(x/2+1),會更快。這個地方也是卡住我了好久的地方之一。另一個地方就是程式碼細節處理,在下方會有註釋說明。
//--------------------------------------------------------------------------------
const int N = 2e5 + 10;
const int M = 1e6 + 10;
const int mod = 1e9 + 7;
const int INF = 1e16;
int n, m, T;
vector<int> ans;
//mp是存數用的,mp3用來撤回的。
map<int, int> mp3, mp;
int A[N];
//--------------------------------------------------------------------------------
bool dfs(int x) {
if (mp[x] > 0) {
mp[x]--;
mp3[x]++;
return 1;
}
if ((x % 2 == 0) || (x == 1)) return 0;
return (dfs(x / 2) and dfs(x / 2 + 1));
}
signed main() {
// 記得註釋
// fileRead();
kuaidu();
T = 1;
// cin >> T;
while (T--) {
cin >> n;
rep(i, 1, n) {
int a; cin >> a;
A[i] = a;
mp[a]++;
// mp2[a]++;
}
sort(A + 1, A + n + 1, [&](int a, int b) {return a > b; });
rep(i, 1, n) {
int a = A[i];
if (mp[a] <= 0) continue;
//要先減去,否則會對dfs函式產生影響,這是第二個卡住了我n年的地方。
mp[a]--;
mp3。clear();
if (dfs(a + 1)) {
mp[a + a + 1]++;
continue;
}
//如果return false的話就把之前刪除的恢復回來
for (auto k : mp3) mp[k。first] += k。second;
mp3。clear();
if (dfs(a - 1)) {
mp[a + a - 1]++;
continue;
}
for (auto k : mp3) mp[k。first] += k。second;
mp3。clear();
mp[a]++;
}
for (auto [a, b] : mp) {
if (a <= 0) continue;
while (b > 0) {
b--;
ans。push_back(a);
}
}
// sort(ans。begin(), ans。end(), [&](int a, int b) { return a > b; });
cout << ans。size() << endl;
for (auto x : ans) { cout << x << " "; }
}
return 0;
}
Problem C. 書包與最長上升子序列
這個題是想不出來看著蔣老師的 vp 錄影的程式碼搞明白的,在此膜拜一下,太厲害啦。
首先小於 10000 的時候直接特判就好了。當比較大的時候,我們採取用 012 去給他補滿,假設這個輸入的數 x 是一個 12 的倍數,我們如果能用最短的長度湊滿呢?
首先是基本的
012012012012012012012012012012
但是在這其中每多一個 012,增長的貢獻就會很大,所以我們會在中間插入若干個 2,例如變成
01201201201222012012201222201201201222
至於該怎麼插入,插入多少,就寫在程式碼裡了。
那如果 x 不是 12 的倍數呢?
前面暴力列舉三個數字,使得 x 減去他們之後是 12 的倍數就好了。
signed main() {
//記得註釋
// fileRead();
kuaidu();
T = 1;
//cin >> T;
while (T--) {
cin >> n;
if (n == 0) {
cout << 0 << endl;
continue;
}
if (n <= 100000) {
rep(i, 1, n) cout << 1;
continue;
}
string s = "";
rep(i, 3, 9) rep(j, i + 1, 9) rep(p, j + 1, 9) {
int val = i * 100 + j * 10 + p;
if (n % 12 != 0 and (n >= val) and (n - val) % 12 == 0) {
s += i + '0';
s += j + '0';
s += p + '0';
n -= val;
}
}
int num = n / 12;
if (num == 1) {
s = s + "012";
}
else {
m = 1;
//這是所有的012產生貢獻的式子表達
while (m * (m + 1) * (m + 2) / 6 <= num) m++;
m -= 1;
num -= m * (m + 1) * (m + 2) / 6;
//要倒著列舉,這樣更大。從大往下處理。
rep2(i, m, 1) {
//這個式子是第i位的2能夠提供的貢獻
int x = i * (i + 1) / 2;
用num除以x就是我們貪心個數的極限
A[i] = num / x;
num -= x * A[i];
}
rep(i, 1, m) {
s += "012";
rep(j, 1, A[i]) s += "2";
}
}
cout << s << endl;
}
return 0;
}
/*
*/
PostScript
這場自己 v 的其實挺爛的,最近感覺寫題動力不足呢,水平也上不去,感覺現在已經是半退役狀態了。。。