簽到:EF
銅牌題:BJ
銀牌題:HILM
金牌題:G...
B Bitwise Exclusive-OR Sequence
\(n\) 個數,\(m\) 個關系,每個關系形如 \(a_u\oplus a_v = w\),表示第 \(u\) 個數與第 \(v\) 數的異或運算結果為 \(w\)。求是否有這樣的\(n\) 個數滿足所有關系要求,如果沒有輸出 -1,如果有輸出所有滿足要求的方案中,所有數字的和的最小值。
\(n \le 100000,m\le 200000,w\lt 2^{20}\)
可以看做 \(n\) 個點,\(m\) 條邊的無向圖,每個連通塊內都通過一些關系使得兩兩可達。所以,如果連通塊內的一個數字確定,那么所有的數字都可以確定下來。
可以選擇連通塊中的某個點作為根 \(rt\),那么可以使用 DFS 求出整個連通塊中所有點與 \(rt\) 的異或值。這個過程可能會檢測出問題無解的情況。由此,可以按 30 位去考慮,計算出整個連通塊中每個點與 \(rt\) 異或值的每一位為 0 和 1 的個數,然后選擇較小的作為答案。為什么可以選擇較小的,因為 \(rt\) 的實際值是由我們去確定的,假設第 \(i\) 位上,與 \(rt\) 異或為 0 的個數為 \(a\),為 1 的為 \(b\),假設 \(a<b\),那么可以讓 \(rt\) 的實際值的第 \(i\) 位為 1,這樣的話,連通塊中只會產生 \(a*2^i\) 的貢獻。復雜度為 \(O(30n)\)
也可以建 30 次圖去跑,不過每次重新建圖容易被卡常數。也可以每次考慮判定二分圖,並利用並查集縮點,即異或為 0 的去合並,異或為 1 的連邊,然后去判斷二分圖。實際測試起來也非常快。
#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 100010, M = 400010;
int n, m, vis[N], val[N], cnt[32][2];
int head[N], ver[M], nxt[M], edge[M], tot;
bool flag;
void add(int x, int y, int z){
ver[++tot] = y, nxt[tot] = head[x], edge[tot] = z, head[x] = tot;
}
void dfs(int x, int fa = 0) {
if(!flag) return;
vis[x] = 1;
for(int i=30;i>=0;i--){
cnt[i][val[x] >> i & 1] ++;
}
for(int i=head[x];i;i=nxt[i]) {
int y = ver[i];
if(vis[y]) {
if((val[y] ^ val[x]) != edge[i]) {
flag = false;
return;
}
} else {
val[y] = val[x] ^ edge[i];
dfs(y, x);
}
}
}
int main(){
scanf("%d%d", &n, &m);
for(int i=1;i<=m;i++){
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
add(x, y, w); add(y, x, w);
}
flag = true;
ll res = 0;
for(int i=1;i<=n;i++){
if(!vis[i]) {
memset(cnt, 0, sizeof cnt);
dfs(i);
for(int j=30;j>=0;j--){
res += 1ll * min(cnt[j][0], cnt[j][1]) * (1 << j);
}
}
}
if(!flag) {
puts("-1"); return 0;
}
printf("%lld\n", res);
return 0;
}
E Edward Gaming, the Champion
判斷一個串中出現了多少次 "edgnb"
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
char s[N];
int main(){
string s;
cin >> s;
int n = s.length();
int res = 0;
for(int i=0;i<=n-5;i++){
if(s.substr(i, 5) == "edgnb") {
res ++;
}
}
cout << res << endl;
return 0;
}
F Encoded Strings I
給定一個長度為 \(n(n\le 1000)\) 的字符串\(s\),定義函數 \(F_s\):
\(F_s(c) = chr(G(c,S))\) ,表示將 S 中的每個字符 c 轉換為 \(chr(G(c,S))\),其中 \(G(c,S)\) 表示 S 中最后一次出現 c 之后的后綴中不同的字符個數,\(chr(i)\) 表示第 \(i\) 個字符(第個0字符是 a,第1個是 b...)。
要求你對 \(s\) 的每個非空前綴代入到函數中,得到 \(n\) 個字符串,輸出按照字典序排序的最大字符串。
難在題意理解,字符串 \(aacc\) 有前綴 \(a,aa,aac,aacc\),它們代入 \(F_s\) 得到:\(a,aa,bba,bbaa\),答案是 \(bbaa\)。
首先預處理出每種字符最后出現的位置,再預處理出每個后綴出現的字符個數,暴力計算即可。(實現方法有很多)
#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 1010;
int n;
char s[N];
int last[26], suf[N];
string get(int n) {
unordered_map<char, int> mp;
for(int i=n;i>=1;i--){
suf[i] = mp.size();
mp[s[i]] ++;
}
for(int i=1;i<=n;i++){
last[s[i] - 'a'] = i;
}
string t = "";
for(int i=1;i<=n;i++){
t += char('a' + suf[last[s[i] - 'a']]);
}
return t;
}
int main(){
scanf("%d%s", &n, s + 1);
string res = "";
for(int i=1;i<=n;i++) {
res = max(res, get(i));
}
cout << res << endl;
}
G Encoded Strings II
給定一個長度為 \(n(n\le 1000)\),且僅包含 \(a\sim t\) 的字符串,求對它的所有非空子序列使用 \(F_s\) 函數處理后,字典序最大的結果。
字符串最多包含 20 種字符,這個條件可以被我們所利用。
假如字符串只包含了\(k\) 種字符,那么最終答案肯定是 \(k\) 段連續字符,因為首個字符字典序要最大。
然后枚舉 \(k\) 種字符,在子序列中出現的順序,然后對於每一段字符,起始位置是固定的,嘗試去找結束維護。
假設現在處理第 i 個字符,它代表了答案的第 \(i\) 段字符,由於希望答案盡可能的字典序更大,所以我們希望第 \(i\) 段字符也盡可能的長。
另外需要保證其他 \(k-i\) 種字符在后面可以出現,所以處理 \(f[s]\) 表示 \(k-i\) 種字符構成的二進制集合 \(s\),使得這 \(k\) 種字符都出現的最短后綴出現的位置。
現在有了第 \(i\) 段字符可選的區間范圍 \(l,r\),\(r\) 對於每種字符可能不同,我們想找到這個范圍里面,出現次數最多的字符,然后枚舉它,繼續遞歸。遞歸時,下次搜索的左端點是當前枚舉字符在 \(r\) 之前最后一次出現的位置。
#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 1005;
// sum 是前綴和,last 表示每個字符出現的最后位置,f[i] 表示 i 集合字符出現的最早后綴位置,pre[i][j] 表示 j 字符在 i 位置之前出現的最近位置。
int n, sum[N][20], last[20], f[1<<20], pre[N][20];
int cnt, num[20], vis[20];
char s[N];
void dfs(int u, int l) {
if(u == cnt) return;
int st = 0; // 找到還沒有分配的字符集合 st
for(int i=0;i<20;i++) if(!vis[i] && last[i]) st |= 1 << i;
int mx = 0; // 在 l,r 中找到最多的出現次數
for(int i=0;i<20;i++) {
if(!vis[i] && last[i]) {
int r = f[st ^ (1 << i)] - 1; // st 集合中除去 i, 剩下的字符都出現的最短后綴位置。
mx = max(mx, sum[r][i] - sum[l-1][i]);
}
}
if(mx < num[u]) return; // 剪枝,第 u 段長度不夠更新
if(mx > num[u]) { // 足夠去更新第 u 段字符的長度,那么后面所有的長度都歸 0
num[u] = mx;
for(int i=u+1;i<cnt;i++) num[i] = 0;
}
for(int i=0;i<20;i++) { // 枚舉每個出現次數為 mx 的字符 i
if(!vis[i] && last[i]) {
int r = f[st ^ (1 << i)] - 1;
int x = sum[r][i] - sum[l-1][i];
if(x == mx) {
vis[i] = 1;
dfs(u + 1, pre[r][i] + 1); // 下次搜索的起點,應當是 r 之前最后一次出現 i 字符的后一個位置。
vis[i] = 0;
}
}
}
}
int main(){
scanf("%d%s", &n, s + 1);
// 預處理
for(int i=1;i<=n;i++){
memcpy(sum[i], sum[i-1], sizeof sum[i]);
memcpy(pre[i], pre[i-1], sizeof pre[i]);
int c = s[i] - 'a';
sum[i][c] ++;
pre[i][c] = i;
if(!last[c]) cnt++;
last[c] = i;
}
// 預處理 f[i]
f[0] = n + 1;
for(int i=0;i<(1<<20);i++) {
for(int j=0;j<20;j++) {
if(i >> j & 1) {
f[i] = min(f[i ^ (1 << j)], last[j]);
break;
}
}
}
dfs(0, 1);
for(int i=0;i<cnt;i++) {
for(int j=0;j<num[i];j++) putchar('a' + (cnt - i - 1));
}
puts("");
}
J Luggage Lock
長度為 4 的密碼鎖,每次可以選擇連續的一段數字,向上或者向下撥動 1。求從 \(a_0a_1a_2a_3\) 轉動到 \(b_0b_1b_2b_3\) 的最小步數。
正解是任何一組轉移都可以轉換成從 \(0000\) 開始轉到 \(abcd\),這樣可以 \(BFS\) 進行預處理然后 \(O(1)\) 回答。復雜度 \(O(T)\)。(每次轉移考慮一個區間,向上或者向下轉,共 20 種轉移)
另外一種解法是考慮每個數字最終是向上轉還是向下轉(可以想想它不會既向上又向下),假設 \(a\) 到 \(b\) 可以向上轉 \(x\) 步,也可以向下轉 \(10-x\) 步,但只考慮這兩種轉移,即考慮 \(2^4\) 種方案並不能AC,有些數字可能會多轉一圈。(不過還沒有找到特殊情況),對拍的話發現需要考慮向上轉 \(x,10+x\),向上轉 \(10-x,20-x\) 等四種方案,即 \(4^4=256\) 種方案,然后求最小值。復雜度 \(O(256T)\)
#include <bits/stdc++.h>
using namespace std;
int T;
char s[10], t[10];
int a[10][5], b[10];
int res;
int sgn(int x) {
if (x == 0) return 0;
return x > 0 ? 1 : -1;
}
void dfs(int x) {
if (x == 5) {
int ans = 0;
for (int k = 1; k <= 4; k++)
{
if (b[k] == 0) continue;
int p = 4;
for (int j = k; j <= 4; j++)
if (sgn(b[k]) != sgn(b[j])) { p = j - 1; break; }
int last = 0;
for (int c = k; c <= p; c++)
{
int kk = 0;
if (b[c] < 0) kk = -b[c];
else kk = b[c];
if (kk >= last) ans += kk - last;
last = kk;
}
k = p;
}
res = min(res, ans);
return;
}
for (int i = 0; i < 4; i++) {
b[x] = a[x][i];
dfs(x + 1);
}
}
int main() {
scanf("%d", &T);
while (T--) {
res = 1e9;
scanf("%s %s", s + 1, t + 1);
for (int i = 1; i <= 4; i++) {
int c = s[i] - t[i];
if (c < 0) c = 10 + c;
a[i][0] = c;
a[i][1] = c - 10;
a[i][2] = c + 10;
a[i][3] = c - 20;
}
dfs(1);
printf("%d\n", res);
}
return 0;
}
H Line Graph Matching
\(n\) 個點,\(m\) 條邊的帶權無向連通圖,將其轉換為線圖即,原圖中的點變成邊,邊變成點,如果原圖中兩個邊有公共點,那么就在線圖中對應的兩個點連相應的帶權值的邊,權值為原圖兩個邊的權值和。求線圖中的最大權點匹配,即選出來一些邊,它們沒有公共點並且權值和最大。
\(n\le 10^5,m\le 2*10^5\)
最大權點匹配要求每兩個點需要有公共邊,線圖中的點即原圖中的邊,所以原問題可以轉換為最大權邊匹配。只要有兩個邊有公共點,就可以匹配。
題目給定的是一個連通圖,如果邊數為偶數,那么一定可以全部匹配,如果邊數為奇數,那么需要選擇一條邊刪去。(假想的結論)
但是刪除邊是有要求的,如果要刪的是割邊,那么需要保證刪除之后每個連通塊邊的數量都是偶數。如果刪的不是割邊,則不需要任何限制。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100010, M = 400010;
int head[N], ver[M], nxt[M], edge[M], vis[N];
int dfn[N], low[N], cnt[N], n, m, tot, num;
int Min;
void add(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z, nxt[tot] = head[x], head[x] = tot;
}
void tarjan(int x, int in_edge) {
dfn[x] = low[x] = ++num;
for (int i=head[x]; i; i = nxt[i]) {
int y = ver[i];
if (!dfn[y]) {
tarjan(y, i);
cnt[x] += cnt[y] + 1; // x 所在的連通分量邊數增加
low[x] = min(low[x], low[y]);
if (low[y] > dfn[x]) {
if (cnt[y] % 2 == 0) { // 如果該邊是割邊,那么要判斷 y 所在連通分量邊數是否為偶數
Min = min(Min, edge[i]);
}
}
else { // 如果不是割邊,則不需要限制
Min = min(Min, edge[i]);
}
}
else if(i != (in_edge ^ 1)) { // 如果出現返祖邊,即搜索樹中,子孫指向祖先的邊
if(dfn[x] > dfn[y]) { // 這條邊記錄到時間戳更大的點上(只是為了防止重復計數)
cnt[x] ++;
}
low[x] = min(low[x], dfn[y]);
Min = min(Min, edge[i]);
}
}
}
int main() {
scanf("%d%d", &n, &m);
tot = 1;
ll res = 0;
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z); add(y, x, z);
res += z;
}
Min = 1e9; // 如果要刪邊,Min 記錄可以刪除的邊中的最小權值
tarjan(1, 0);
if(m & 1) res -= Min;
cout << res << endl;
return 0;
}
I Linear Fractional Transformation
根據線性變換保交比性,可以直接推出答案。
#include <bits/stdc++.h>
using namespace std;
typedef long double db;
const db PI = acos(-1.0);
struct Complex {
db x, y;
Complex(db _x = 0.0, db _y = 0.0) {
x = _x;
y = _y;
}
Complex operator - (const Complex& b) const {
return Complex(x - b.x, y - b.y);
}
Complex operator + (const Complex& b) const {
return Complex(x + b.x, y + b.y);
}
Complex operator * (const Complex& b) const {
return Complex(x * b.x - y * b.y, x * b.y + y * b.x);
}
Complex operator / (const Complex& p) const {
db a = x, b = y, c = p.x, d = p.y;
return Complex((a * c + b * d) / (c * c + d * d), (b * c - a * d) / (c * c + d * d));
}
bool operator == (const Complex& b) const {
return x == b.x && y == b.y;
}
void input() {
scanf("%Lf%Lf", &x, &y);
}
void output() {
printf("%.10Lf %.10Lf\n", x, y);
}
}z[5], w[5];
int main() {
int T;
cin >> T;
while (T--) {
for (int i = 1; i <= 3; i++) {
z[i].input();
w[i].input();
}
z[4].input();
bool flag = false;
for (int i = 1; i <= 3; i++) {
if (z[i] == z[4]) {
w[i].output();
flag = true;
break;
}
}
if (flag) continue;
Complex r = ((z[4] - z[1]) * (z[3] - z[2]) * (w[3] - w[1])) / ((z[4] - z[2]) * (z[3] - z[1]) * (w[3] - w[2]));
r = (w[1] - r * w[2]) / (Complex(1, 0) - r);
r.output();
}
return 0;
}
L Perfect Matchings
\(2n\) 個點的完全圖,從中刪除 \(2n-1\) 條邊,保證這 \(2n-1\) 條邊構成一棵樹,求在剩下的圖中找到 \(n\) 條無公共點的邊的方案數。答案對 \(998244353\) 取模。
\(n\le 2000\)
問題可以轉換為在 \(2n\) 個點的樹上,匹配 \(n\) 個點對保證每個點對在樹上沒有邊直接相連。
-
首先,\(2n\) 個點兩兩配對,方案數是 \(d_{n}={C_{2n}^nn!\over {2^n}}=\prod_{i=1}^n 2i-1\)
首先 \(C_{2n}^n\) ,分成兩部分,然后第一部分的每個元素依次選擇另一部分去配對,方案數是\(n!\)。然后由於每個點對是無序的,共 \(n\) 對,所以要除以 \(2^n\)。最后展開可以得到等式右側。
-
考慮容斥模型,條件 \(P_i\) 表示第 \(i\) 條邊不選,那么包含 \(P_i\) 條件的集合為 \(S_i\),而 \(\overline S_i\) 表示不包含第 \(i\) 條邊的方案數。假設共 \(m=2n-1\) 條邊,那么我們要求的是:
\[\left|\bigcap_{i=1}^{m}S_i\right|=|U|-\left|\bigcup_{i=1}^m\overline{S_i}\right| \]該公式表示 \(m\) 條邊都不選擇的方案數,就是題目的答案。\(|U|\) 表示 \(2n\) 個點任意配對,每條邊都沒有限制。后者需要用容斥原理去求。
每次考慮 \(x\) 個條件 \(P_i\) 不被滿足,即指定 \(x\) 條邊一定要選,那么它對答案的貢獻符號是 \((-1)^x\)。
我們不需要一一枚舉每個大小為 \(x\) 的條件集合去求並,然后去求並集大小計算貢獻,因為總的集合數量太多,而我們只對集合大小感興趣。
所以,可以直接計算當指定 \(x\) 條邊一定被選,而其他 \(2n-2x\) 個點任意匹配時的方案數。
設 \(f_i\) 表示指定 \(i\) 條邊的方案數,然后還需要乘上 \(d_{n-i}\) 才行,表示其他 \(2*(n-i)\) 個點兩兩任意配對。這組方案數表示了所有大小為 \(i\) 的條件集合並的集合大小之和。
-
考慮 \(f_i\) 的求解,需要使用樹形DP,具體的,我們需要用狀態 \(f_{x,i,0}\) 表示 \(x\) 子樹中,選擇了 \(i\) 條邊並且 \(x\) 沒有被選擇時的方案數,而 \(f_{x,i,1}\) 相應表示 \(x\) 被選擇時的方案數。
所以就可以遍歷 \(x\) 的每個子節點 \(y\),更新:
-
\(f_{x,i,0}*(f_{y,j,0}+f_{y,j,1}) \Rightarrow f_{x,i+j,0}\)
-
\(f_{x,i,1}*(f_{y,j,0}+f_{y,j,1})\Rightarrow f_{x,i+j,1}\)
-
\(f_{x,i,0}*f_{y,j,0} \Rightarrow f_{x,i+j+1,1}\)
更新思路使用了背包的滾動壓縮思想,所以枚舉 \(i\) 和 \(j\) 時,需要倒序枚舉。這個DP復雜度看起來是 \(O(n^3)\),但因為 \(i\) 和 \(j\) 的遍歷范圍會根據 \(x\) 和 \(y\) 的子樹大小有所限制,所以實際復雜度是 \(O(n^2)\)。
這里復雜度大概可以理解為樹上每兩個點配對去計算。
對於 \(x\) 的每個子節點構成的子樹,每個\(sz_y\) 需要與 \(sz_x\) 遍歷一遍,大概等於 \(sz_x\) 中的每個點與其他點配對的次數。
-
由於我對容斥的理解有限,所以這里多花了一點篇幅介紹容斥思想。
#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 4005, mod = 998244353;
int n;
vector<int> g[N];
ll f[N][N][2], sz[N], p[N];
void dfs(int x, int fa = 0){
f[x][0][0] = sz[x] = 1;
for(auto &y : g[x]) {
if(y == fa) continue;
dfs(y, x);
for(int i = sz[x] / 2; i >= 0; i--) {
for(int j = sz[y] / 2; j >= 0; j--){
if(j > 0) {
(f[x][i + j][0] += f[x][i][0] * (f[y][j][1] + f[y][j][0]) % mod) %= mod;
(f[x][i + j][1] += f[x][i][1] * (f[y][j][1] + f[y][j][0]) % mod) %= mod;
}
(f[x][i + j + 1][1] += f[x][i][0] * f[y][j][0] % mod) %= mod;
}
}
sz[x] += sz[y];
}
}
int main(){
scanf("%d", &n);
for(int i=1;i<2*n;i++){
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1);
ll res = 0;
p[0] = 1;
for(int i=1;i<=n;i++) p[i] = p[i-1] * (2 * i - 1) % mod;
for(int i=0;i<=n;i++){
if(i & 1) (res -= (f[1][i][0] + f[1][i][1]) * p[n - i] % mod) %= mod;
else (res += (f[1][i][0] + f[1][i][1]) * p[n - i] % mod) %= mod;
}
printf("%lld\n", (res + mod) % mod);
}
M String Problem
對於字符串的每個前綴,輸出其字典序最大的子串。只需要輸出左右端點即可。
長度 \(10^6\)
第一種做法是使用后綴自動機。第二種是 KMP 自動機。
具體后面補,貼一個KMP代碼
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
int nxt[N];
char s[N];
int main(){
scanf("%s", s+1);
int n = strlen(s + 1);
int head = 1, len = 1;
printf("%d %d\n", 1, 1);
for(int i=2;i<=n;i++){
if(s[i] > s[head]) {
head = i, len = 1;
printf("%d %d\n", head, head);
continue;
}
while(nxt[len] != 0 && s[head + nxt[len]] < s[i]) {
head = head + len - nxt[len];
len = nxt[len];
}
if(s[head + nxt[len]] == s[i]) {
nxt[len + 1] = nxt[len] + 1;
len ++;
} else if(s[head + nxt[len]] > s[i]) {
nxt[++len] = 0;
}
printf("%d %d\n", head, head + len - 1);
}
return 0;
}
