蒟蒻の估分
作為一個學了一年多還只在入門組的高齡 \(OIer\),\(T1\) 居然寫掛了……
\(T1\) 是一道簡單的數學題,考場上把問題想得太過復雜了,答案居然是由4個值中的最大值來決定的,鬼知道我是怎么想到的,期望得分 \(80/100 ~ pts\)
\(T2\) 是純暴力,題目保證了修改次數不超過 \(5000\),自然是為 \(O(n^2)\) 修改、\(O(1)\) 查詢奠定下了剛好卡時過的基礎。但是考場上我用的方法是 \(O(1)\) 修改、\(O(n)\) 查詢,似乎能過吧,期望得分 \(76/100 ~ pts\)。
\(T3\) 是一道大大的模擬,判斷\(ERR\) 屬實揪心,一大堆特判,從此恨上地址,花了大概 \(1h\),好在樣例 \(3\) 給得十分的良心,還是調出來了,期望得分 \(100/100 ~ pts\)。
\(T4\) 的時候,我旁邊的“大佬”就開始發作了,那臟話飆的啊(畢竟在三亞這一小漁村嘛),心態被影響了,最后最簡單的暴力也沒打出來,期望得分 \(10/100 ~ pts\)。
綜上,這次的 \(CSP-J\) 的期望總分就是 \(266/400 ~ pts\)。
題解
T1 分糖果
給定的 \(L\) 和 \(R\) 的范圍到了恐怖的 \(10^9\),遍歷出解顯然不現實,考慮能否 \(O(1)\) 出解。
再仔細讀一遍題,捋清楚邏輯后不難發現:對於這道題來說,對答案有貢獻的就是 \(L\) 和 \(R\) 除以 \(n\) 后的余數,可以分兩種情況討論:
- 當 \(R - L \ge n\) 時,根據抽屜原理,可以得到在這段區間內一定有一個數 \(x\) 滿足 \(x \mod n = n - 1\),直接輸出 \(n - 1\) 即可。
- 當 \(R - L < n\) 時,毫無頭緒,那就再分情況:
- 如果存在一個正整數 \(k\) 滿足 $ L < kn \le R$ (也即 \(L \div n \ne R \div n\))時,不難發現可以通過拿 \(kn -1\) 個糖果來使獎勵糖果數量最大,也可以直接輸出 \(n - 1\)。
- 如果不存在正整數 \(k\),則令 \(k\) 為保證 \(kn \le L\) 的最大值。此時,\(L,R\) 的關系就是 \(kn \le L \le R < (k + 1)n\),自然是拿的越多余數越大,輸出 \(R \mod n\) 即可。
上代碼:
#include <bits/stdc++.h>
using namespace std;
int n, L, R;
int main() {
scanf("%d%d%d", &n, &L, &R);
if (L / n != R / n) printf("%d", n - 1);
else printf("%d", R % n);
return 0;
}
T2 插入排序
做這道題的時候,沒看見下面的修改操作不超過5000次,於是就有了 \(O(1)\) 修改、\(O(n)\) 查詢的考場代碼。
主要思想是在這一題的情況下,對於數組中的一個數 \(a_x\),其排序后的位置取決於它前面所有小於它的數的個數的和它后面所有小於等於它的數的個數之和。
#include <bits/stdc++.h>
#define MAXN 8100
using namespace std;
int n, Q, a[MAXN];
int main() {
scanf("%d%d", &n, &Q);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
int op, x, v;
while (Q--) {
scanf("%d", &op);
if (op == 1) {
scanf("%d%d", &x, &v);
a[x] = v;
} else {
scanf("%d", &x);
int ans = 0;
for (int i = 1; i <= x; i++) {
if (a[i] <= a[x]) ans++;
}
for (int i = x + 1; i <= n; i++) {
if (a[i] < a[x]) ans++;
}
printf("%d\n", ans);
}
}
return 0;
}
當然,上面的做法會 \(TLE\),也就希望 \(O2\) 能救救我。
正解來了(\(O(\log n)\) 的修改和 \(O(\log n)\) 的查詢)
既然題目給出了排序不會對后面的結果產生影響,那么我們大可不必在 \(a\) 數組中排序,而是可以采用一個 \(vector\) 類型的 \(f\) 數組來存儲排序后的序列。
其次,每一次修改都只是單點修改,這就意味着之前的排序結果可以重用,進而降低時間復雜度。具體怎么重用呢?這就是接下來的重點了:
對於每一次修改,我們需要:
- 在 \(f[]\) 中找到並刪去這個元素。
- 往 \(f[]\) 中推入新的元素,同時維護 \(f[]\) 的單調遞增性。
而對於每一次詢問 \(a[x]\) 排序后的位置,只需要通過 \(a[x]\) 的值找到其在 \(f[]\) 中的位置,再將其輸出就行了。
乍一看,兩次操作都是 \(O(n)\) 時間復雜度的啊,哪來的 \(O(\log n)\) 呢?
仔細想想,對於每一次操作,我們都要掃一遍整個 \(f[]\) 來查找對應位置,而 \(f[]\) 本身是單調遞增的,這就意味着一個新的優化誕生了:二分優化查找過程!
只要每次在 \(f[]\) 中查找對應位置時均用二分優化,無論是修改還是查詢,時間復雜度都會降到優秀的 \(O(\log n)\)。
上代碼:
#include <bits/stdc++.h>
#define MAXN 8100
using namespace std;
int n, Q;
struct Node {
int v, id;
bool operator<(const Node &rhs) const {
if (v == rhs.v) return id < rhs.id;
return v < rhs.v;
}
} a[MAXN];
vector<Node> f;
int main() {
scanf("%d%d", &n, &Q);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i].v);
a[i].id = i;
f.insert(lower_bound(f.begin(), f.end(), a[i]), a[i]);
}
int op, x, v;
while (Q--) {
scanf("%d", &op);
if (op == 1) {
scanf("%d%d", &x, &v);
f.erase(lower_bound(f.begin(), f.end(), a[x]));
a[x].v = v;
f.insert(lower_bound(f.begin(), f.end(), a[x]), a[x]);
} else {
scanf("%d", &x);
printf("%d\n", lower_bound(f.begin(), f.end(), a[x]) - f.begin() + 1);
}
}
return 0;
}
T3 網絡連接
巨大無比的模擬,判斷 \(ERR\) 屬實揪心。
因為對 \(STL\) 掌握不熟練,所以考場上索性放棄寫 \(map\)。
好了言歸正傳,判斷 \(ERR\) 時,除了題目中已經給出的情況,還有很多的特判,列舉如下:
- \(:\) 的個數少於 \(1\) 個或 \(.\) 的個數少於 \(3\) 個。
- \(:\) 出現在 \(.\) 的前面。
- 不以數字開頭。
- \(.\) 或 \(:\) 后面沒有數字(體現為以 \(.\) 或 \(:\) 結尾或二者直接相鄰)。
- ……
判斷完 \(ERR\),這題也就完成一半了。
因為每一次操作都需要訪問之前的服務機,所以我用了一個 \(ser\) 數組來存儲對應服務機的下標。
然后就挨個遍歷每一台機器,先判斷地址是否 \(ERR\),然后再根據首字母判斷機器類型:是服務機則與之前所有服務機比對,有相同則輸出 \(FAIL\),反之輸出 \(OK\);是客戶機則與之前所有服務機比對,一旦有相同,立馬輸出對應服務器在初始序列中的下標並 \(break\),如果一直沒有相同的,則輸出 \(FAIL\)。
比對兩串字符是否相同就是裸裸的按位比對。
關於兩台服務機地址相同則后者 \(FAIL\) 的問題,客戶機在連接時也是從前往后遍歷的,因此只會連接上第一台出現此地址的服務器,完美地解決了這個問題。
#include <bits/stdc++.h>
#define MAXN 1100
using namespace std;
int n, ser[MAXN];
char op[MAXN][10], ad[MAXN][30];
bool check(int x) {
int ln = strlen(ad[x] + 1);
int cnt1 = 0, cnt2 = 0;
long long t = 0;
if (!('0' <= ad[x][1] && ad[x][1] <= '9')) return 1;
if (ad[x][ln] == '.' || ad[x][ln] == ':') return 1;
for (int i = 1; i <= ln; i++) {
if (ad[x][i] == '.') {
if (ad[x][i - 1] == '.' || ad[x][i - 1] == ':') return 1;
if (cnt2) return 1;
if (ad[x][i + 1] == '0' && ('0' <= ad[x][i + 2] && ad[x][i + 2] <= '9')) return 1;
cnt1++;
} else if (ad[x][i] == ':') {
if (ad[x][i - 1] == '.' || ad[x][i - 1] == ':') return 1;
if (ad[x][i + 1] == '0' && ('0' <= ad[x][i + 2] && ad[x][i + 2] <= '9')) return 1;
cnt2++;
} else {
t = (t << 3) + (t << 1) + ad[x][i] - '0';
if (ad[x][i + 1] == '.' || ad[x][i + 1] == ':') {
if (t > 255) return 1;
t = 0;
}
}
}
if (cnt2 != 1 || t > 65535) return 1;
return 0;
}
bool cmp(char a[], char b[]) {
int lena = strlen(a + 1), lenb = strlen(b + 1);
if (lena != lenb) return 0;
for (int i = 1; i <= lena; i++) {
if (a[i] != b[i]) return 0;
}
return 1;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%s%s", op[i] + 1, ad[i] + 1);
if (op[i][1] == 'S') ser[++ser[0]] = i;
}
for (int i = 1; i <= n; i++) {
if (check(i)) {
printf("ERR\n");
continue;
}
bool fl = 0;
if (op[i][1] == 'S') {
for (int j = 1; j <= ser[0] && ser[j] < i; j++) {
if (cmp(ad[i], ad[ser[j]])) {
fl = 1;
printf("FAIL\n");
break;
}
}
if (!fl) printf("OK\n");
} else {
for (int j = 1; j <= ser[0] && ser[j] < i; j++) {
if (cmp(ad[i], ad[ser[j]])) {
fl = 1;
printf("%d\n", ser[j]);
break;
}
}
if (!fl) printf("FAIL\n");
}
}
return 0;
}
然而,\(AC\) 了洛谷上的民間數據,在 \(CCF\) 卻掛成了可憐的 \(65\),去逛了逛討論區,發現了一個奇妙的數據:
1
Server 1.1.1.1:9999999999999999
用 \(int\) 來存 \(a, b, c, d, e\) 必然會爆,再考慮 \(long ~ long\),題目中給出地址串的長度至多為 \(25\),除去 \(3\) 個 .
,一個 :
,以及極限狀態下的 \(4\) 個 \(1\),留給最后一個極大值的位數只有 \(17\) 位了,\(long ~ long\) 毫無壓力地存下。
還是過不去 \(\dots\dots\)
然后又思索了一會兒,發現對於這樣一個數據:
1
Client 090.228.145.77:8080
沒判出來 \(ERR\),原因是對於 \(a\) 中的前導零根本沒有判斷,加上之后就得到了正解。
同時,比對可以用 \(map\) 來優化,讀入數字的時候可以用一個不大常用的東西 \(sscanf\)。
#include <bits/stdc++.h>
using namespace std;
int n;
char type[30], ad[30];
map<string, int> mp;
bool check() {
int len = strlen(ad), cnt1 = 0, cnt2 = 0;
if (ad[0] == '0' && ('0' <= ad[1] && ad[1] <= '9')) return 1;
for (int i = 1; i <= len; i++) {
if (ad[i] == '.') {
cnt1++;
if (ad[i + 1] == '0' && ('0' <= ad[i + 2] && ad[i + 2] <= '9')) return 1;
} else if (ad[i] == ':') {
cnt2++;
if (ad[i + 1] == '0' && ('0' <= ad[i + 2] && ad[i + 2] <= '9')) return 1;
}
if (cnt1 > 3 || cnt2 > 1) return 1;
}
long long a, b, c, d, e;
if (sscanf(ad, "%lld.%lld.%lld.%lld:%lld", &a, &b, &c, &d, &e) != 5) return 1;
if (a > 255 || b > 255 || c > 255 || d > 255 || e > 65535) return 1;
return 0;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%s%s", type, ad);
if (check()) {
printf("ERR\n");
continue;
}
if (type[0] == 'S') {
if (mp[ad]) printf("FAIL\n");
else {
mp[ad] = i;
printf("OK\n");
}
} else {
if (!mp[ad]) printf("FAIL\n");
else printf("%d\n", mp[ad]);
}
}
return 0;
}
T4 小熊的果籃
最后一道題了,肯定會稍微的有些難度(考完試返程途中才想到正解太淦了)。
直接來講 \(100pts\) 的思路吧,這題滿分解法有很多,起碼我現在看到的就有 \(3\) 種了,但我選擇的必定是代碼最短最好理解的。
因為每一次拿走水果都相當於是從序列里刪除一個元素,那么就可以用到鏈表。
再順着往下推,推出正解也就不遠了。
可以用一個 \(vector\) 容器(令其為 \(b\))來存儲每個塊的塊頭,然后不斷更新。具體如何更新呢?更新必須滿足條件:去掉塊頭后這個塊內仍然有元素。否則跳過(那塊都消失了你還管人家干啥)。
最最最后的代碼了
#include <bits/stdc++.h>
#define MAXN 200100
using namespace std;
int n, a[MAXN], l[MAXN], r[MAXN];
vector<int> b;
int main() {
scanf("%d", &n);
a[0] = a[n + 1] = -1, r[0] = 1, l[n + 1] = n;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
if (a[i] != a[i - 1]) b.push_back(i);
l[i] = i - 1, r[i] = i + 1;
}
while (r[0] != n + 1) {
vector<int> tmp;
for (int i = 0; i < b.size(); i++) {
printf("%d ", b[i]);
int u = l[b[i]], v = r[b[i]];
r[u] = v, l[v] = u;
if (a[b[i]] != a[u] && a[b[i]] == a[v]) tmp.push_back(v);
}
puts("");
b = tmp;
}
return 0;
}
蒟蒻の得分
終究還是掛了 \(\dots\dots\)