簽到題:EDGI
銅題:H
銀題:JKM
金牌:B
D Strange Fractions
設 \(x = {a \over b}\),那么有 \({p\over q} = x + {1 \over x}\) ,可以轉換為求解 \(qx^2-px+q = 0\) 的正整數根。
使用求根公式,判斷 \(p^2-4*q^2\) 是否是完全平方數即可。然后取 \({a\over b}={p+d\over 2q}\) 。復雜度 \(O(T\log q)\)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int T;
ll p, q;
int main(){
scanf("%d", &T);
while(T--){
scanf("%lld%lld", &p, &q);
ll d = p * p - 4 * q;
if(d < 0) {
puts("0 0");
} else {
ll t = sqrt(d);
if(t * t == d) printf("%lld %lld\n", p + t, 2 * q);
else puts("0 0");
}
}
}
E Strange_Integers
整體排序后,貪心的去選即可。證明:設 \(f[i]\) 表示前 \(i\) 個能夠選出的最多的個數,那么 \(f[i]\) 隨着 \(i\) 遞增,求解 \(f[j]\) 時(\(j\gt i\)),去找盡量大的 \(i\) 滿足 \(a[j]-a[i] \ge k\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, k, a[N];
int main(){
cin >> n >>k;
for(int i=1;i<=n;i++){
scanf("%d", &a[i]);
}
sort(a + 1, a + 1 + n);
int res = 1;
int last = a[1];
for(int i=2;i<=n;i++){
if(a[i] - last >= k) {
last = a[i];
res ++;
}
}
cout <<res <<endl;
return 0;
}
G Edge Groups
\(n\) (奇數)個點的樹,\(n-1\) 條邊分成 \({n-1\over 2}\) 組,每組兩條邊並且這兩條邊要有一個公共點,詢問分組的方案數。
考慮樹形DP,子樹 \(x\) 中若有偶數個點,那么奇數條邊必然無法分組,需要 \(x\) 連向其父親的邊。如果有奇數個點,那么偶數條邊可以分組。
設 \(d[x]\) 為分組 \(x\) 子樹中的邊的方案數,設 \(x\) 的孩子 \(y\) 中,有 \(a\) 個 \(y\) 需要 \((x,y)\) 這條邊與 \(y\) 子樹中的邊配對,有 \(b\) 個 \(y\) 是 \((x,y)\) 在 \(x\) 這里配對的。那么當 \(b\) 是奇數時還需要 \(x\) 與其父親連接的邊。
也就是說,\(x\) 有 \(a\) 個子樹點數是偶數,\(b\) 個子樹點數是奇數。現在只需要考慮把 \(b\) 個子樹兩兩分組即可,如果 \(b\) 是奇數,那么還需要 \((x,fa)\) 加入其中。
\(n\) 個元素,每組兩個分成 \({n\over 2}\) 組的方案數是 \(f[n]\),有遞推式 \(f[n] = f[n-2] * (n-1)\) 。
所以:
每個子樹的方案數依舊獨立,所以累計貢獻時還是用乘法,只是其中 \(b\) 個是有順序的,這個順序個數就是 \(f\)。
#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, mod = 998244353;
int n, sz[N];
vector<int> g[N];
ll d[N], f[N];
void dfs(int x, int fa){
sz[x] = 1; d[x] = 1;
int cnt = 0;
for(auto &y : g[x]) {
if(y == fa) continue;
dfs(y, x);
sz[x] += sz[y];
d[x] = d[x] * d[y] % mod;
if(sz[y] & 1) cnt ++;
}
if(cnt & 1) cnt ++;
d[x] = d[x] * f[cnt] % mod;
}
int main(){
scanf("%d", &n);
for(int i=1;i<n;i++){
int x, y; scanf("%d%d", &x, &y);
g[x].push_back(y); g[y].push_back(x);
}
f[0] = 1;
for(int i=2;i<=n;i+=2){
f[i] = f[i-2] * (i-1) % mod;
}
dfs(1, 0);
printf("%lld\n", d[1]);
return 0;
}
H Life is a Game
\(n\) 個點 \(m\) 條邊,有點權和邊權,\(q\) 個詢問,每個詢問 \(x,k\) 表示從 \(x\) 出發,帶着 \(k\) 的能力,通過一條邊的條件是能力大於這條邊的權值,當第一次到達一個點時可以將該點的權值累加到能力上,求可以獲得的最大能力。
考慮離線詢問,每個點帶有若干個詢問,每個詢問包含初試能力值和詢問ID。然后從小到大枚舉每條邊\((x,y,w)\),設 \(v[x]\) 是 \(x\) 的點權,那么對於從 \(x\) 出發的詢問,如果能力值 \(k+v[x] < w\),那么說明它無法到達除去 \(x\) 之外的所有點,它的答案就是 \(k+v[x]\),緊接着把該詢問從 \(x\) 的詢問集合中刪除。之后對於 \(y\) 做同樣的處理。
此時,\(x\) 和 \(y\) 中詢問都可以通過 \((x,y,w)\) 這條邊,所以,可以將 \(x\) 與 \(y\) 合並成一個集合,然后該集合的點權和就是 \(v[x]+v[y]\)。這里可以用一個帶權並查集合並。另外詢問也需要合並,按照啟發式合並的思想,每次小的集合向大的集合合並即可。
總體復雜度為 \(O(m\log m+q\log ^2 q)\) 。
關於啟發式合並的思想簡單提一下,每次小的集合向大的集合合並,那么對於小的集合而言,大小擴大二倍以上,所以每個點從小的集合刪除再加入大的集合的次數不會超過 \(\log_2^n\),整體復雜度就是 \(n\log_2^n\)。本題中因為需要對詢問按照能力值排序,所以需要用 multiset 維護,所以是兩個 log 的復雜度即 \(q\log^2 q\)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100010;
int n, m, q;
struct Edge
{
int x, y, w;
bool operator<(const Edge &b) const
{
return w < b.w;
}
} e[N];
int f[N], v[N], rs[N];
multiset<pair<int, int>> st[N];
int get(int x) { return x == f[x] ? f[x] : f[x] = get(f[x]); }
int main()
{
cin >> n >> m >> q;
for (int i = 1; i <= n; i++)
scanf("%d", &v[i]), f[i] = i;
for (int i = 1; i <= m; i++)
{
int x, y;
scanf("%d%d%d", &e[i].x, &e[i].y, &e[i].w);
}
sort(e + 1, e + 1 + m);
for (int i = 1; i <= q; i++)
{
int x, k;
scanf("%d%d", &x, &k);
st[x].insert({k, i});
}
int p = 1;
for (int i = 1; i <= m; i++)
{
int x = e[i].x, y = e[i].y, w = e[i].w;
x = get(x);
y = get(y);
if (x == y)
continue;
// 從 x 出發的集合無法跨過 w
while (st[x].size() && (*st[x].begin()).first < w - v[x])
{
int id = st[x].begin()->second;
rs[id] = v[x] + st[x].begin()->first;
st[x].erase(st[x].begin());
}
while (st[y].size() && (*st[y].begin()).first < w - v[y])
{
int id = st[y].begin()->second;
rs[id] = v[y] + st[y].begin()->first;
st[y].erase(st[y].begin());
}
// 啟發式合並,從小的集合合並到大的集合中
if (st[x].size() > st[y].size())
swap(x, y);
while (st[x].size())
{
st[y].insert(*st[x].begin());
st[x].erase(st[x].begin());
}
f[x] = y;
v[y] += v[x];
}
// 還有一些詢問留在最后處理
for (int i = 1; i <= n; i++)
{
int x = get(i);
for (auto &t : st[x])
{
rs[t.second] = v[x] + t.first;
}
st[x].clear();
}
for (int i = 1; i <= q; i++)
{
printf("%d\n", rs[i]);
}
}
I Steadily Growing Steam
\(n\) 個卡牌,有價值 \(v_i\) 和點數 \(t_i\) ,你需要從中選出兩組卡牌使得其點數和相等,然后最大化這兩組的價值和。另外你可以選擇不超過 \(k\) 個不同的卡牌,使得其 \(t_i\) 變成 \(2t_i\)。
\(k \le n \le 100, t_i\le 13, |v_i|\le 10^9\)。
考慮背包,我們僅關心這兩組的點數和之差,所以可以把這個當做背包體積,如果第 \(i\) 個卡牌放入第一個集合,體積增加 \(t_i\),否則體積減少 \(t_i\),最后只需要取體積為 0 時候的答案即可。
另外還有一個體積是使卡牌點數翻倍的次數,所以狀態可以定義為 \(d[i][j][w]\),表示前 \(i\) 張卡牌,翻轉恰好 \(j\) 次,兩個集合體積差為 \(w\) 時的最大價值和。那么轉移方程有:
此時,時間復雜度 \(O(n^3t_{max})\),其中 \(t_{max} = 26\) 。注意到空間復雜度也是 \(O(n^3t_{max})\),顯然需要滾動數組優化。
#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 = 105, M = 2605;
int n, k, v[N], t[N], T = 1300;
ll d[2][N][M];
/*
t_max = 26, 總和是 2600,但兩個集合的差最多只需要維護 [-1300,1300] 即可,因為差距更大的話最終不會回到 0
數組訪問下標不能為負數,所以加一個偏移量 T 即可。
*/
int main(){
scanf("%d%d", &n, &k);
rep(i,1,n) scanf("%d%d", &v[i], &t[i]);
memset(d, 0xcf, sizeof d); // v[i] 范圍是 [-1e9,1e9],所以答案提前初始化為負無窮
d[0][0][T] = 0; // 一開始什么都不選,答案為 0
int z = 1; // 滾動數組的下標
for(int i=1;i<=n;i++){
memset(d[z], 0xcf, sizeof d[z]);
for(int j=0;j<=k;j++){
for(int w=-1300;w<=1300;w++){
for(int p=-2;p<=2;p++){ // 5 種物品,體積分別為 -2t[i],-t[i],0,t[i],2t[i],對應的價值為 v[i],v[i],0,v[i],v[i]
int wt = w + p * t[i]; // 轉移后的體積
if(wt < -1300 || wt > 1300) continue; // 超出范圍就過濾掉
if(j > 0) { // j > 0 表示 5 種物品都可以考慮轉移
// 轉移代碼簡略了一些,第一個點是 abs(p) = 2 時,要從 j - 1轉移;第二個點是 p != 0 時價值為 v[i]
d[z][j][wt+T] = max(d[z][j][wt+T], d[z^1][j - (abs(p) == 2)][w + T] + (p == 0 ? 0 : v[i]));
} else if(abs(p) <= 1) { // j = 0 只能考慮不加倍點數的轉移
d[z][j][wt+T] = max(d[z][j][wt+T], d[z^1][j - (abs(p) == 2)][w + T] + (p == 0 ? 0 : v[i]));
}
}
}
}
z^=1;
}
ll res = 0;
// 狀態定義為恰好翻倍 k 次的最大價值,所以要遍歷所有的 [1,k]。
for(int i=0;i<=k;i++) res = max(res, d[n&1][i][T]);
printf("%lld\n", res);
}
J Two Binary Strings Problem
給出兩個長度為 \(n(n\le 50000)\) 的 01 串 \(A,B\),對於每一個 \(1\le k\le n\),遍歷對於所有的 \(1\le i \le n\),定義 $C_i $ 為集合 \(\{A_{max(i-k+1,1)},...,A_{i-1},A_i\}\) 里面的眾數(題目定義:如果 1 的個數嚴格多於一半,就是 1,否則是 0),如果每個 \(C_i = B_i\) 成立,那么就輸出 1,否則輸出 0。
題意很繞,舉個例子:

\(A = 0110011\),對於每個 \(K\),可以將其轉換成每一行上的 \(C\),如果第 \(i\) 行上的 \(C=B\),那么就輸出 1,否則輸出 0。
這題需要找規律遞推,不妨借助這個圖,去發現一些規律。
考慮對於每個 \(A_i\),一次性求出 \(k\in [1,n]\) 時的所有 \(C_i\)。可以用 bitset 快速維護,並且利用之前的答案遞推。
當前枚舉 \(i\),試圖找到一個最近的 \(j\),滿足 \(A_{j+1},\cdots,A_i\) 中 0 和 1 的個數相同。考慮如果借助計算出的 \(n\) 個 \(C_j\) 去遞推 \(n\) 個 \(C_i\)。
如果 \(A_i=1\),那么當 \(k = 1,2,\cdots,i-j-1\) 時,\(C_i=1\)。因為這一段數字中,1的個數永遠比 0 的個數多;當 \(k=i-j\) 時,\(C_i=0\),因為此時 0 和 1 的個數恰好一樣多。那么當 \(k=t>i-j\) 時,就可以使用 \(C_j\) 去遞推了。原因在於每個位置,1 和 0 的大小關系是等同的。
遞推例子:

細節上如何遞推:第 \(i\) 列的 \(n\) 個數字可以看做一個 \(n\) 位二進制(第 \(p\) 位對應 \(k=p+1\) 時的 \(C_i\)),使用 bitset 維護,叫做 \(f_i\)
找到上面描述的那個 \(j\),如果找不到:
- 如果 \(A_i=1\),那么 \(f_i\) 的所有 \(n\) 位都是 1
- 如果 \(A_i=1\),那么 \(f_i\) 的所有 \(n\) 位都是 0
如果找到了 \(j\):
- 如果 \(A_i = 1\),那么 \(f_i\) 的 \(0\sim i-j-2\) 位都是 1(對應 \(k=1,\cdots,i-j-1\)),第 \(i-j-1\) 位是 0。然后 \(f_i = f_i | (f_{j}\ll (i-j))\)
- 如果 \(A_i=0\),那么 \(f_i\) 的 \(0\sim i-j-1\) 位都是 0,然后 \(f_i = f_i|(f_j\ll (i-j))\)
最后要計算所有合法的 \(k\),用一個長度為 \(n\) 的二進制數 \(st\) 表示合法狀態,起初所有位上都是 1,遍歷所有 \(i\),只需要每次與\(f_i\) 中的合法狀態做位與運算即可。當 \(B_i=1\) 時,\(f_i\) 中為 1 的位合法,否則 \(f_i\) 中為 0 的位合法。
其他詳細情況可見代碼。細節蠻多,可以考慮下標從 0 開始,會省去二進制數字中第 0 位的特殊處理。
#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 = 50010;
int T, n;
char a[N], b[N];
bitset<N> c[N], cn, rs;
void solve(){
unordered_map<int,int> mp; mp[0] = -1;
int dis = 0;
// 先制作一個 0~n-1 位都是 1 的二進制數字
cn.reset(); rs.reset();
for(int i=0;i<n;i++) cn[i] = 1;
rs = cn; // 保存答案,也就是合法狀態 st
for(int i=0;i<n;i++){
dis += (a[i] == '1' ? 1 : -1);
c[i].reset(); // c[i] 全部置為 0
if(mp.count(dis)) {
int j = mp[dis];
if(a[i] == '1') { // c[i] 的 0 ~ i-j-2 位都是1
c[i] |= cn >> (n - (i - j - 1));
}
if(j >= 0) // 如果 j = -1,不需要或運算
c[i] |= c[j] << (i - j);
} else { // 不存在 j
if(a[i] == '1') c[i] = cn;
}
if(c[i][i] == 1) { // 如果看不懂,思考上面的 C_3 的后綴 1
c[i] |= (cn >> i) << i;
}
mp[dis] = i;
if(b[i] == '1') {
rs &= ~(c[i] ^ cn);
} else {
rs &= ~c[i];
}
}
for(int i=0;i<n;i++) {
if(rs[i] == 1) putchar('1');
else putchar('0');
}
puts("");
}
int main(){
scanf("%d", &T);
while(T--){
scanf("%d", &n);
scanf("%s%s", a, b);
solve();
}
}
K Circle of Life
構造一個長度為 \(n\) 的 01 串,每一次變換的規則如下:
- 第 \(i\) 位如果為 1,那么變換后第 \(i+1\) 和第 \(i-1\) 位為 1。最左邊和最右邊的溢出不需要管。
- 如果\(i\) 和\(i+2\) 位都為 1,那么這兩個 1 會在 \(i+1\) 發生沖撞抵消,第 \(i+1\) 位變換后為 0。
- 如果 \(i\) 和 \(i+1\) 位都為 1,那么變換后 \(i\) 和 \(i+1\) 位也為 0。
要求變換前與變換后,串內必須有 1,並且你需要保證構造出的串,可以在 \(2n\) 次變換內,出現兩個完全相同的串。
打表找規律即可。附打表代碼:
#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;
bool check(string s){
for(auto &c : s) if(c == '1') return true;
return false;
}
bool ok(string s) {
unordered_map<string,int> mp;
int cnt = 0;
while(check(s)) { // 每次變換需要保證二進制串中有 1
mp[s] = 1;
string t = s;
for(int i=0;i<s.length();i++){ // 遍歷 s 的每一位
t[i] = '0';
int flag = i > 0 ? s[i-1] == '1' : false; // 查看 s[i-1] 是否為 1
int flag2 = i + 1 < s.length() ? s[i + 1] == '1' : false; // 查看 s[i+1] 是否為 1
int flag3 = s[i] == '1'; // 查看 s[i] 是否為 1
// t[i] 為 1 當且僅當 (s[i-1] 或 s[i+1] 其中有一個是 1 並且 s[i] 為 0)
if(flag + flag2 + flag3 == 1 && flag3 == 0) t[i] = '1';
}
if(mp[t]) return true; // 如果出現相同,則合法
s = t;
cnt ++;
if(cnt > 2 * s.length()) return false; // 如果變換次數超過上限,返回false
}
return false;
}
void solve(int n) {
cout << "group " << n << " : "<< endl;
for(int i=0;i<(1<<n);i++){ // 二進制枚舉
string s = "";
for(int j=0;j<n;j++){
if(i >> j & 1) s += "1";
else s += "0";
}
if(ok(s)) {
cout << s << endl;
// break;
}
}
}
int main(){
int n; // 輸入 n,找到長度為 n 的所有合法串
cin >> n;
solve(n);
}
打出表來之后,可以先觀察長度為 2、3、4、5、6、7、8 等長度的串。發現可以使用 1001 作為一個單元去構造后面延長的循環串。
AC代碼:
#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;
int n;
string s[] = {"0", "0", "01", "", "1001", "10001", "011001", "0101001"};
void solve() {
cin >> n;
if (n == 3) {
puts("Unlucky");
} else {
if (n <= 7) {
cout << s[n] << endl;
} else {
int cnt = 0;
if (n % 4 == 0) {
cnt = n / 4;
} else if (n % 4 == 1) {
cout << s[5];
cnt = n / 4 - 1;
} else if (n % 4 == 2) {
cout << s[2];
cnt = n / 4;
} else {
cout << s[7];
cnt = n / 4 - 1;
}
for (int i = 0; i < cnt; i++)
cout << "1001";
cout << endl;
}
}
}
int main() { solve(); }