2024.4.20 筆記
SP4354 Snowflakes
記錄所有的雪花,判斷是否存在兩個雪花是相同的。由於資料量較大,需要 \(O(n)\) 的複雜度來查詢雪花,考慮雜湊表
定義一個雜湊值的轉換方式,讓不同的雪花雜湊值不相同,相同的雪花的六個角一定是相同的 \(6\) 個值且相同的順序排列,只不過起點在不同的角上。因此可以將雜湊值定義為每朵雪花的六個角的長度之和 \(+\) 六個角的長度乘積。
然後還需要判斷兩個雪花是否相同,不能使用雜湊值比較的方法,因為可能會產生雜湊衝突,因此可以使用雪花的特性,兩個相同的雪花,各自從某一角開始順時針或逆時針記錄長度,能得到兩個相同的六元組。我們可以基於這個特性直接暴力判斷。
int n;
int snow[N][6];
int h[N], ne[N], idx;
int t[6];
int get_hash(int a[])
{
int res1 = 0, res2 = 0;
for (rint i = 0; i < 6; i++)
{
res1 = (res1 + a[i]) % P;
res2 = (res2 * a[i]) % P;
}
return (res1 + res2) % P;
}
bool check(int a[], int b[])
{
for (rint i = 0; i < 6; i++)
{
for (rint j = 0; j < 6; j++)
{
bool flag = 1;
for (rint k = 0; k < 6; k++)
if (a[(i + k) % 6] != b[(j + k) % 6])
flag = 0;
if (flag) return 1;
flag = 1;
for (rint k = 0; k < 6; k++)
if (a[(i + k) % 6] != b[(j - k) % 6])
flag = 0;
if (flag) return 1;
}
}
return 0;
}
bool insert(int a[])
{
int x = get_hash(a);
for (rint i = h[x]; i; i = ne[i])
{
if (check(snow[i], a)) return 1;
}
idx++;
for (rint i = 0; i < 6; i++) snow[idx][i] = a[i];
ne[idx] = h[x];
h[x] = idx;
return 0;
}
signed main()
{
cin >> n;
while (n--)
{
for (rint i = 0; i < 6; i++)
{
cin >> t[i];
}
if (insert(t))
{
puts("Twin snowflakes found.");
return 0;
}
}
puts("No two snowflakes are alike.");
return 0;
}
AcWing 138. 兔子與兔子
本題每次要比較的是字串中的某兩個區間是否相同,可以用字串雜湊來做,只需要使該區間內雜湊值一樣即可
int n, m;
char s[N];
//h[i] 表示原字串中前 i 個字元組成的字串的雜湊值
//p[i] 表示 p 的 i 次方
uint h[N], p[N];
uint calc(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
signed main()
{
scanf("%s", s + 1);
n = strlen(s + 1);
p[0] = 1;
for (rint i = 1; i <= n; i++)
{
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + s[i];
}
cin >> m;
while (m--)
{
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
if (calc(l1, r1) == calc(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}
AcWing 139. 迴文子串的最大長度
由於 zty 講的是雜湊,就不用manacher了
本題要求的是一個字串中最大回文串的長度,我們可以列舉中間點,然後每次求出當前中間點的最大回文串,對所有情況取一個最大值即可。
但是對於中間點有兩種情況,如果字串是奇數個,那麼是存在中間點的,但是如果字串是偶數個,那麼是不存在中間點的。這裡我們可以用一個常用技巧來簡化判斷,將字串中每兩個字元之間加上一個特殊字元,假設加上一個 '#'
,
對於奇數個的字串,a#b#c#d#f
,新增後還是奇數個。對於偶數個的字串,a#b#c#d
,新增後變成了奇數個。透過這樣的處理,我們只需要考慮奇數情況的字串就行了,奇數個的字串一定是存在中間點的,因此直接列舉中間點即可。
然後就要對於每個中間點求最大回文串的長度,可以求當前中間點兩邊需要加上的邊長,然後二分求這個邊長的最大值。每次二分出最大值後統計一下回文串的長度,更新最大值即可。
int n;
char s[N];
//h[] 表示正序的字串雜湊值
//rh[] 表示倒序的字串雜湊值
//p[i] 表示p的i次方
uint h[N], rh[N], p[N];
uint calc(uint h[], int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
signed main()
{
int T = 1;
while (scanf("%s", s + 1), strcmp(s + 1, "END"))
{
n = strlen(s + 1);
for (rint i = n * 2; i >= 1; i -= 2)
//在字串的每兩個字元之間插入一個相同的數
{
s[i] = s[i / 2];
s[i - 1] = 'z' + 1;
}
n *= 2; //更新字串的長度
p[0] = 1;
for (rint i = 1, j = n; i <= n; i++, j--)
{
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + s[i];
rh[i] = rh[i - 1] * P + s[j];
}
int res = 0;
//記錄最大回文串的長度
for (rint i = 1; i <= n; i++)
//列舉中間值
{
int l = 0, r = min(i - 1, n - i);
while (l < r)
{
int mid = (l + r + 1) >> 1;
//如果兩邊的字串相等說明當前邊長已經是迴文串,那麼可以繼續擴大邊長
if (calc(h, i - mid, i - 1) == calc(rh, n - (i + mid) + 1, n - (i + 1) + 1)) l = mid;
else r = mid - 1;
//否則說明不是迴文串,那麼更大的邊長也不能組成迴文串,因此需要縮小邊長
}
if(s[i - l] <= 'z') res = max(res, l + 1);
//如果頭和尾是字串中的字元,那麼整個迴文串的長度是邊長+1
else res = max(res, l);
//如果頭和尾是額外新增的特殊字元,那麼整個迴文串的長度就是邊長
}
printf("Case %lld: %lld\n", T++, res);
}
return 0;
}
P3435 OKR-Periods of Words
本題是一個字串關於迴圈元的證明。
這裡直接得出結論:對於字串中每一位 i
,s[i - ne[i] + 1 ~ i]
和 s[1 ~ ne[i]]
都是相等的,並且不存在更大的 ne
值滿足這個條件
還能得出推論:最小迴圈節是 1-ne[i]
,次小迴圈節是 1-ne[ne[i]]
,依次能得出一個字串所有的迴圈節。
void get_next(char p[], int n)
{
for (rint i = 2, j = 0; i <= n; i++)
{
while (j > 0 && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j++;
ne[i] = j;
}
}
signed main()
{
scanf("%lld%s", &n, s + 1);
get_next(s, n);
for (rint i = 2, j = 2; i <= n; i++, j = i)
{
while (ne[j]) j = ne[j];
if (ne[i]) ne[i] = j;//記憶化一下,不然會 TLE 30pts
ans += i - j;
}
cout << ans << endl;
return 0;
}
P5410 【模板】擴充套件 KMP
學的 George1123 佬的
這裡只給出程式碼
int ans1, ans2;
int z[M];
char a[N], b[N];
char new_s[M];
void exKMP_getZ(char s[])
{
int n = strlen(s);
for (rint i = 1, j = 0; i < n; i++)
{
int k = i - j;
if (z[j] - k > 0) z[i] = min(z[k], z[j] - k);
while (z[i] + i < n && s[z[i]] == s[z[i] + i]) z[i]++;
if (z[j] - z[i] < k) j = i;
}
}
signed main()
{
scanf("%s%s", a, b);
int la = strlen(a);
int lb = strlen(b);
for (rint i = 0; i < lb; i++) new_s[i] = b[i];
for (rint i = lb, j = 0; i < la + lb; i++, j++) new_s[i] = a[j];
exKMP_getZ(new_s);
for (rint i = 0; i < lb; i++)
{
if (!i) ans1 ^= lb + 1;
else ans1 ^= (min(z[i], lb - i) + 1) * (i + 1);
}
for (rint i = 0; i < la; i++) ans2 ^= (min(z[i + lb], lb) + 1) * (i + 1);
cout << ans1 << endl << ans2 << endl;
return 0;
}
AcWing 142. 字首統計
本題要求的是已知若干個字串,然後查詢出有多少個字串是給定查詢的字串的字首。
關於字首的統計可以用 Trie 樹來做,將已知的字串全部加入 Trie 樹中,在每個字串的結尾節點做上標記。
然後在 Trie 樹上查詢給定的字串,在查詢這個字串的路上到達的所有字首都是字串的字首,每走到一個節點就將標記上累計的字串個數累加到結果上。
int n, m;
int tr[N][27], tot = 1;
int cnt[N];
char s[N];
void insert(char s[])
{
int len = strlen(s), p = 1;
for (rint k = 0; k < len; k++)
{
int ch = s[k] - 'a';
if (!tr[p][ch]) tr[p][ch] = ++tot;
p = tr[p][ch];
}
cnt[p]++;
}
int search(char s[])
{
int len = strlen(s), p = 1;
int ans = 0;
for (rint k = 0; k < len; k++)
{
p = tr[p][s[k] - 'a'];
if (!p) return ans;
ans += cnt[p];
}
return ans;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++)
{
scanf("%s", s);
insert(s);
}
for (rint i = 1; i <= m; i++)
{
scanf("%s", s);
cout << search(s) << endl;
}
return 0;
}
AcWing 143. 最大異或對
字典樹不單單可以高效儲存和查詢字串集合,還可以儲存二進位制數字
將每個數以二進位制方式存入字典樹,找的時候從最高位去找有無該位的異
void insert(int val)
{
int p = 1;
for (rint k = 30; k >= 0; k--)
{
int ch = val >> k & 1;
if (!tr[p][ch]) tr[p][ch] = ++tot;
p = tr[p][ch];
}
}
int search(int val)
{
int p = 1;
int ans = 0;
for (rint k = 30; k >= 0; k--)
{
int ch = val >> k & 1;
if (tr[p][ch ^ 1])
{ // 走相反的位
p = tr[p][ch ^ 1];
ans |= 1 << k;
}
else
{ // 只能走相同的位
p = tr[p][ch];
}
}
return ans;
}
signed main()
{
cin >> n;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
insert(a[i]);
ans = max(ans, search(a[i]));
}
cout << ans << endl;
return 0;
}
AcWing 144. 最長異或值路徑
首先可以用深搜求出所有點到根節點的異或距離,由於在二進位制中異或運算相當於減法,
因此對於 x->y
之間的異或路徑長度即可求解
我們現在需要列舉所有點,對於每個點x都求出和它的異或路徑的異或值最大的一個點 \(y\),那麼從 \(x\) 能走到的最長的異或路徑也可求解
要從 \(n\) 個數中選出兩個數,使得這兩個數的異或值最大,可以使用 Trie 樹快速求解
void add(int a, int b, int c)
{
e[++idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx;
}
void dfs(int x, int father, int sum)
{
a[x] = sum;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (y == father) continue;
dfs(y, x, sum ^ w[i]);
}
}
void insert(int val)
{
int p = 1;
for (rint k = 30; k >= 0; k--)
{
int ch = val >> k & 1;
if (!tr[p][ch]) tr[p][ch] = ++tot;
p = tr[p][ch];
}
}
int search(int val)
{
int p = 1;
int ans = 0;
for (rint k = 30; k >= 0; k--)
{
int ch = val >> k & 1;
if (tr[p][ch ^ 1])
{
p = tr[p][ch ^ 1];
ans |= 1 << k;
}
else
{
p = tr[p][ch];
}
}
return ans;
}
signed main()
{
cin >> n;
for (rint i = 1; i < n; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
add(b, a, c);
}
dfs(0, 0, 0);
for (rint i = 1; i <= n; i++) insert(a[i]);
int res = 0;
for (rint i = 1; i <= n; i++) res = max(res, search(a[i]));
cout << res << endl;
return 0;
}
AcWing 147. 資料備份
可以發現最優解中每兩個配對的辦公樓一定時相鄰的,因此我們可以計算一下每兩個相鄰的辦公樓之間的距離。
d[i]
表示第 \(i\) 個辦公樓和第 \(i+1\) 個辦公樓之間的距離。
那麼問題就變成了從 d[]
數列中選 \(k\) 個數,使它們的和最小,並且相鄰的兩個數不能被同時選(任一辦公樓都屬於唯一的配對組)
如果 k = 1
,答案就是 d[]
數列中的最小值。
如果 k = 2
,答案則一定是以下兩種情況:
-
- 選擇最小值
d[i]
,以及除了d[i - 1], d[i], d[i + 1]
之外的其他數中的最小值
- 選擇最小值
-
- 選擇最小值兩側的兩個數,
d[i - 1]
和d[i + 1]
- 選擇最小值兩側的兩個數,
很容易證明,如果不選 d[i - 1]
和 d[i + 1]
,那麼最優解一定選了 d[i]
,選了 d[i]
後不能選 d[i - 1]
和 d[i + 1]
,因此還選了這三個數以外的最小值。如果選了d[i - 1]或d[i + 1]其中一個,由於d[i]的最小值,那麼這時將 d[i - 1]
或 d[i + 1]
換成 d[i]
答案會更小,因此在最優解只有以上兩種情況,且 d[i]
兩則的數要麼都選要麼都不選。
因此,我們可以先選上最小值 d[i]
,然後把 d[i - 1], d[i], d[i + 1]
從數列中刪去,再在原位置加入 d[i - 1] + d[i + 1] - d[i]
,這時就變成了"從新的數列中選出不超過 \(k-1\) 個數,使它們的和最小,且相鄰兩個數不能同時選"這個子問題。
對於子問題,如果選了 d[i - 1] + d[i + 1] + d[i]
,相當於去掉 d[i]
,換上 d[i - 1]
和 d[i + 1]
。如果沒選,那麼剛才選出的 d[i]
加上這次選出的最小值就是最優解。這樣恰好涵蓋了最優解的兩種情況。
綜上所述,得出了本題的演算法:
建立一個連結串列,連線 \(n-1\) 個節點,分別表示 d[1], d[2], ..., d[n - 1]
,即每兩個辦公樓之間的距離。再建立一個最小堆,
與連結串列構成對映關係(即堆中也有 \(n-1\) 個節點,權值分別是 d[1], d[2], ..., d[n - 1]
,同時記錄對應的在連結串列中的下標)。
每次取出堆頂,把權值累加到答案中,設堆頂對應連結串列節點的下標為 p
,數值為 w[p]
,在連結串列中刪除 p, p->prev, p->next
,
在同樣的位置插入一個新節點 q
,記錄數值 w[q] = w[p->prev] + w[p->next] - w[p]
。在堆中同時刪除對應的 p->prev
和 p->next
的節點,
插入對應連結串列節點 q
,權值為 w[q]
的新節點。
重複上述操作 \(K\) 次,就得到了最終答案。
int n, k;
int d[N];
int l[N], r[N];
//連結串列
int idx;
bool st[N];
//記錄某個節點是否被刪去
priority_queue<pii, vector<pii>, greater<pii> > h;
void remove(int x)
{ //刪除連結串列中某個元素
st[x] = 1;
//記錄當前節點在堆中也被刪除
r[l[x]] = r[x];
l[r[x]] = l[x];
}
signed main()
{
cin >> n >> k;
for (rint i = 0; i < n; i++) cin >> d[i];
for (rint i = n - 1; i > 0; i--) d[i] -= d[i - 1];
d[0] = d[n] = inf; //設定兩個邊界哨兵
for (rint i = 1; i < n; i++)
{
l[i] = i - 1;
r[i] = i + 1;
h.push({d[i], i});
//加入堆中
}
int res = 0; //記錄最小總和
for (rint i = 0; i < k; i++)
{
while (st[h.top().second]) h.pop();
//將所有應該刪去的節點刪去
pii t = h.top();
//取出堆頂
h.pop();
int v = t.first;
int p = t.second, left = l[p], right = r[p];
remove(left), remove(right); //刪去兩邊的節點
res += v; //累加值
d[p] = d[left] + d[right] - d[p]; //修改值
h.push({d[p], p}); //將修改後的節點放回堆中
}
cout << res << endl;
return 0;
}
AcWing 241. 樓蘭圖騰
從左向右依次遍歷每個數 \(a[i]\),使用樹狀陣列統計在 \(i\) 位置之前所有比 \(a[i]\) 大的數的個數、以及比 \(a[i]\) 小的數的個數。統計完成後,將 \(a[i]\) 加入到樹狀陣列。
從右向左依次遍歷每個數 \(a[i]\),使用樹狀陣列統計在 \(i\) 位置之後所有比 \(a[i]\) 大的數的個數、以及比 \(a[i]\) 小的數的個數。統計完成後,將 \(a[i]\) 加入到樹狀陣列。
int n, a[N];
int l[N], r[N];
int c[N];
int A, V;
int lowbit(int x) {return x & -x;}
void add(int x, int k)
{
for (rint i = x; i <= n; i += lowbit(i)) c[i] += k;
}
int ask(int x)
{
int ans = 0;
for (rint i = x; i; i -= lowbit(i)) ans += c[i];
return ans;
}
signed main()
{
cin >> n;
for (rint i = 1; i <= n; i++) cin >> a[i];
for (rint i = 1; i <= n; i++)
{
//因為從左往右遍歷並插值,所以在呼叫 ask 函式時 c 中存的都是第 i 個節點左邊的值
int y = a[i]; //當前節點的高度
l[i] = ask(y - 1); //找到當前節點左邊的比高度比 y 小的數的個數
r[i] = ask(n) - ask(y);//找到當前節點左邊的比高度比 y 大的數的個數
add(y, 1);//把 y 插入到 c 陣列中, 相當於建樹
}
memset(c, 0, sizeof c);
//準備從右往左讀,再建一遍樹
for (rint i = n; i >= 1; i--)
{
int y = a[i];
l[i] *= ask(y - 1);
A += l[i];
//以 y 為最高點的總方案數為 (y 左邊比 y 低的點數) * (y 右邊比 y 低的點數)
r[i] *= ask(n) - ask(y);
V += r[i];
//以 y 為最低點的總方案數為 (y 左邊比 y 高的點數) * (y 右邊比 y 高的點數)
add(y, 1);
}
cout << V << " " << A << endl;
return 0;
}
P3605 Promotion Counting
求某節點子樹內比該節點的點權大的點的個數
int n, p[N], b[N], ans[N];
vector<int> e[N];
int c[N];
int lowbit(int x){ return x & -x;}
void add(int x, int y)
{
for (; x <= n; x += lowbit(x)) c[x] += y;
}
int query(int x)
{
int ans = 0;
for (; x; x -= lowbit(x)) ans += c[x];
return ans;
}
void dfs(int x)
{
ans[x] = query(p[x]) - query(n);
for (auto y : e[x]) dfs(y);
ans[x] += (query(n) - query(p[x]));
add(p[x], 1);
}
signed main()
{
cin >> n;
for (rint i = 1; i <= n; i++)
{
cin >> p[i];
b[i] = p[i];
}
sort(b + 1, b + n + 1);
for (rint i = 1; i <= n; i++)
{
p[i] = lower_bound(b + 1, b + n + 1, p[i]) - b;
}
for (rint i = 2; i <= n; i++)
{
int x;
cin >> x;
e[x].push_back(i);
}
dfs(1);
for (rint i = 1; i <= n; i++) cout << ans[i] << endl;
return 0;
}
P4054 [JSOI2009] 計數問題
定義第一個維度為 \(x\),第二個維度為 \(y\),第三個維度為權值 \(c\)。
定義兩個函式 \(add(x,y,c,d)\) 和 \(sum(x,y,c)\)。
- \(add(x,y,c,d)\):將左上角點座標為 \((1,1)\),右下角點座標為 \((x,y)\) 的矩形中中權值 \(c\) 的格子的個數增加 \(d\)
- \(sum(x,y,c)\) 統計左上角點座標為 \((1,1)\),右下角點座標為 \((x,y)\) 的矩形中權值為 \(c\) 的格子的個數。
因此當進行操作 1 時,將原先的權值出現次數 \(-1\),將修改後的權值的出現次數 \(+1\);
當進行操作 2 時,根據容斥原理,易得答案為 \(sum(x2, y2, c) - sum(x2, y1 - 1, c) - sum(x1 - 1, y2, c) + sum(x1 - 1, y1 - 1, c)\)。
int lowbit(int x) {return x & (-x);}
void add(int x, int y, int k, int color)
{
for (rint i = x; i <= n; i += lowbit(i))
for (rint j = y; j <= m; j += lowbit(j))
c[i][j][color] += k;
}
int query(int x, int y, int color)
{
int ans = 0;
for (rint i = x; i; i -= lowbit(i))
for (rint j = y; j; j -= lowbit(j))
ans += c[i][j][color];
return ans;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++)
{
for (rint j = 1; j <= m; j++)
{
cin >> color;
a[i][j] = color;
add(i, j, 1, color);
}
}
int T;
cin >> T;
while (T--)
{
int op;
cin >> op;
int x1, y1, x2, y2;
if (op == 1)
{
cin >> x1 >> y1 >> color;
add(x1, y1, -1, a[x1][y1]);
a[x1][y1] = color;
add(x1, y1, 1, color);
}
else
{
cin >> x1 >> x2 >> y1 >> y2 >> color;
cout << query(x2, y2, color) - query(x1 - 1, y2, color) - query(x2, y1 - 1, color) + query(x1 - 1, y1 - 1, color) << endl;
}
}
return 0;
}