00:吐槽
今年 \(\texttt{PJ}\) 難度普遍偏低,\(\texttt{T3}\) 質量還不錯。
總結來講:做法顯然、暴力踩正解。
01:優秀的拆分 / power
結論題。
當 \(n\) 為奇數時,無解:因為只有奇數的最低位為 \(1=2^0\)。
否則從高位到低位枚舉輸出就可以了,時間復雜度 \(O(32)\);當然我用的是 \(\texttt{lowbit}\) 運算。
#include <bits/stdc++.h>
#define lowbit(x) (x & -x)
using namespace std;
int stk[64], top = 0;
int main() {
freopen("power.in", "r", stdin);
freopen("power.out", "w", stdout);
int x; scanf("%d", &x);
if(x & 1) puts("-1");
else {
for( ; x; x -= lowbit(x))
stk[++top] = lowbit(x);
while(top--)
printf("%d ", stk[top + 1]);
}
return 0;
}
02:直播獲獎 / live
算法一(50pts)
依據題意直接 \(O(n^2)\) 暴力去找就可以了。
注意題目中所說的
在計算計划獲獎人數時,如用浮點類型的變量(如 C/C++中的 float、double,Pascal 中的 real、double、extended 等)存儲獲獎比例 𝑤%,則計算 5 × 60% 時的結果可能為 3.000001,也可能為 2.999999,向下取整后的結果不確定。因此,建議僅使用整型變量,以計算出准確值。
都是廢話,該怎么用還是怎么用。
算法二(100pts)
注意到每個人的分數值都在 \(600\) 以內,因此我們可以考慮 \(O(n)\) 的排序:桶排。
因為桶排是支持動態插入的,所以可以做這個題目,剩下的依據題意模擬即可,時間復雜度 \(O(600n)\)。
據說有原題,代碼就不放了。
算法三(100pts)
考慮題目所要求的的條件,即每次插入一個數,求其中的第 \(k\) 大,可以想到權值線段樹。
注意查詢的時候查詢的是第 \(k\) 小,因此要注意轉換成第 \(k\) 大,還有要記得離散化。
時間復雜度 \(O(n\log n)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
const int T = N << 2;
#define ls(x) son[x][0]
#define rs(x) son[x][1]
int son[T][2], val[T];
int Newnode() {
static int cnt = 0;
return ++cnt;
}
void update(int p) {
val[p] = val[ls(p)] + val[rs(p)];
}
void insert(int &p, int l, int r, int x) {
if(!p) p = Newnode();
if(l == r) return void(++val[p]);
int mid = (l + r) >> 1;
if(x <= mid) insert(ls(p), l, mid, x);
else insert(rs(p), mid + 1, r, x);
update(p);
}
int find(int p, int l, int r, int k) {
if(l == r) return l;
int mid = (l + r) >> 1;
if(k <= val[ls(p)])
return find(ls(p), l, mid, k);
return find(rs(p), mid + 1, r, k - val[ls(p)]);
}
int a[N], b[N];
int main() {
freopen("live.in", "r", stdin);
freopen("live.out", "w", stdout);
int n, w, m, root = 0;
scanf("%d %d", &n, &w);
for(int i = 1; i <= n; i++)
scanf("%d", a + i), b[i] = a[i];
sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - (b + 1);
for(int i = 1, x; i <= n; i++) {
x = lower_bound(b + 1, b + m + 1, a[i]) - b;
insert(root, 1, m, x);
x = floor(1.0L * w * i / 100.0);
printf("%d ", b[find(root, 1, m, i - max(1, x) + 1)]);
}
return 0;
}
03:表達式 / expr
算法一(30pts)
每次修改暴力修改,然后重復棧的過程,時間復雜度 \(O(q|S|)\)。
算法二(100pts)
考慮每次修改一個點對答案的影響。
把原來給的后綴表達式建成表達式樹,記 \(son_{x,0/1}\) 表示編號為 \(x\) 的節點的左/右兒子。如果當前節點是符號 \(!\) 的話那么只有左兒子。
記 \(f_i=0/1\) 表示這個節點的值取反后對答案有/無影響。
對於表達式中每一個數字,都用其原來的編號,符號節點新建編號,即表達式樹的根的編號為 \(m\)。
表達式樹的所有葉子節點都是數值,非葉子節點都是符號,那么后綴表達式的最后一個符號就是表達式樹的根,顯然 \(f_m=1\)。
接下來考慮標記的下傳,記當前節點為 \(x\):
1、當前符號為 \(!\),那么其子節點的 \(f\) 為 \(1\),否則為 \(0\)。
2、當前符號為 \(\&\),那么若兩個兒子節點的值均為 \(1\),則兩個子節點的 \(f\) 均為 \(1\);若只有一個兒子節點的值為 \(1\),則為 \(0\) 的兒子節點的 \(f\) 為 \(1\);其余情況子節點的 \(f\) 均為 \(0\)。
3、當前符號為 \(|\),那么若兩個兒子節點的值均為 \(0\),則兩個子節點的 \(f\) 均為 \(1\);若只有一個兒子節點的值為 \(1\),則為 \(1\) 的兒子節點的 \(f\) 為 \(1\);其余情況子節點的 \(f\) 均為 \(0\)。
顯然,\(a\&b\) 中,若兩個都為 \(1\),則改變 \(a,b\) 任意一者的值均會改變結果;若只有一個為 \(1\),則只有那個為 \(0\) 的數變為 \(1\) 才會使結果由 \(0\) 變為 \(1\);否則(兩個均為 \(0\))改變其中任何一個都對結果沒有影響。符號為 \(|\) 同理可推出。
那么修改一個節點后的答案,即為節點 \(m\) 的值 異或 當前修改節點的 \(f\)。
時間復雜度 \(O(|S|+q)\)
#include <bits/stdc++.h>
#define ls(x) son[x][0]
#define rs(x) son[x][1]
using namespace std;
const int N = 1e6 + 10;
char c[N];
int son[N][2], val[N], a[N], op[N];
int stk[N], top, n;
bool f[N];
void build(int p) {
if(op[p] == 0)
return void(f[p] = 1);
if(op[p] == 3)
return void(build(son[p][0]));
if(op[p] == 1) {
if(val[ls(p)] and val[rs(p)])
build(ls(p)), build(rs(p));
else if(val[ls(p)]) build(rs(p));
else if(val[rs(p)]) build(ls(p));
return ;
} else {
if(val[ls(p)] and !val[rs(p)])
build(ls(p));
else if(val[rs(p)] and !val[ls(p)])
build(rs(p));
else if(!val[ls(p)] and !val[rs(p)])
build(ls(p)), build(rs(p));
return ;
}
}
int main() {
freopen("expr.in", "r", stdin);
freopen("expr.out", "w", stdout);
scanf("%[^\n]", c + 1);
int size = strlen(c + 1), q, m;
scanf("%d", &n), m = n;
for(int i = 1; i <= n; i++)
scanf("%d", a + i);
for(int i = 1; i <= size; i++) {
if(c[i] == 'x') {
int j = i + 1, x = 0;
while(isdigit(c[j]))
x = x * 10 + (c[j] ^ 48), j++;
i = j;
stk[++top] = x, val[x] = a[x];
} else if(c[i] == '!') {
int p = ++m;
son[p][0] = stk[top--];
val[p] = !val[son[p][0]];
stk[++top] = p, op[p] = 3, i++;
}
else {
int p = ++m;
son[p][0] = stk[top--];
son[p][1] = stk[top--];
if(c[i] == '&')
val[p] = val[ls(p)] & val[rs(p)], op[p] = 1;
else val[p] = val[ls(p)] | val[rs(p)], op[p] = 2;
stk[++top] = p, i++;
}
}
build(m);
scanf("%d", &q);
while(q--) {
scanf("%d", &n);
printf("%d\n", val[m] ^ f[n]);
}
return 0;
}
算法三(玄學)
注意到修改一個點只會修改一條鏈的值,如果數據比較水的話直接整就過了。
如果數據隨機,均攤復雜度是 \(O(q\log |S|)\) 的。
04:方格取數 / number
算法一(20pts)
注意到 \(n,m\) 都很小,可以直接搜索解決。
算法二 (40pts)
\(n,m\) 也不是很大,可以搜索+剪枝解決。
當然值得提出的,這個數據是可以用網絡流解決的。將每個位置拆成兩個點,一個是不取的點,一個是要取的點,然后按照題目所給的能走到的就連邊,跑最大費用最大流即可。
如果數據不怎么卡的話甚至可以過掉 \(70\) 分的數據。
算法三(70pts)
可以考慮最長路解決,但是可能被卡,所以實際得分不一定會有 \(70\),如果寫得好會穩一些。
算法四(70pts)
考慮不會做的題就 \(\texttt{dp}\)。
因為水平方向只有向左走,所以水平的行走(按列行走)是沒有后效性的。
記 \(sum_{i,j}\) 表示走到第 \(i\) 列,前 \(j\) 行的 \(a_i\) 的前綴和,\(f_{i,j}\) 表示走到點 \((i,j)\) 所能達到的最大值,答案即為 \(f_{n,m}\)。
考慮按列轉移,當前為第 \(i\) 行第 \(j\) 列,每次枚舉轉移點 \(k\)(第 \(k\) 行):
1、\(k<i\),\(f_{i,j}=\max \{f_{i,j},f_{k,j-1}+ sum_{j,i}- sum_{j,k-1}\}\)
2、\(k=i\),\(f_{i,j}=\max \{f_{i,j},f_{i,j-1}+a_{i,j}\}\)
3、\(k>i\),\(f_{i,j}=\max \{f_{i,j},f_{k,j-1}+ sum_{j,k}- sum_{j,i-1}\}\)
時間復雜度 \(O(n^2m)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int a[N][N], f[N][N], sum[N][N];
int main() {
int n, m;
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d", a[i] + j);
f[1][1] = a[1][1];
for(int i = 2; i <= n; i++)
f[i][1] = f[i - 1][1] + a[i][1];
for(int j = 2; j <= m; j++)
for(int i = 1; i <= n; i++)
sum[i][j] = sum[i - 1][j] + a[i][j];
for(int j = 2; j <= m; j++) {
for(int i = 1; i <= n; i++) {
f[i][j] = f[i][j - 1] + a[i][j];
for(int k = 1; k < i; k++)
f[i][j] = max(f[i][j], f[k][j - 1] + sum[i][j] - sum[k - 1][j]);
for(int k = i + 1; k <= n; k++)
f[i][j] = max(f[i][j], f[k][j - 1] + sum[k][j] - sum[i - 1][j]);
}
}
printf("%d\n", f[n][m]);
return 0;
}
算法五(100pts)
顯然上面的算法需要一個簡單的優化,注意到我們一直在重復累加一些值,事實上是在對上一列的 \(f\) 做前/后綴和。
記 \(f_{i,j,0/1/2}\) 表示從左、上、下走來,那么有轉移:
1、\(f_{i,j,0}=\max(f_{i,j+1,0},f_{i-1,j,1},f_{i+1,j,2})+a_{i,j}\)
2、\(f_{i,j,1}=\max(f_{i,j+1,0},f_{i-1,j,1})+a_{i,j}\)
3、\(f_{i,j,2}=\max(f_{i,j+1,0},f_{i+1,j,2})+a_{i,j}\)
邊界:
\(f_{i,m+1,k}=f_{n+1,j,k}=f_{0,j,k}=f_{i,0,k}=-\inf\)
答案即為 \(\max(f_{1,1,0},f_{1,1,1},f_{1,1,2})\)
其實這也可以理解為上面方程的前綴和優化,時間復雜度 \(O(nm)\)。
#include <bits/stdc++.h>
using namespace std;
const long long inf = 1LL << 60;
const int N = 1e3 + 10;
long long a[N][N], f[N][N], g[N][N][2];
int main() {
freopen("number.in", "r", stdin);
freopen("number.out", "w", stdout);
int n, m;
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%lld", a[i] + j);
for(int i = 0; i <= n + 1; i++)
for(int j = 0; j <= m + 1; j++)
f[i][j] = g[i][j][0] = g[i][j][1] = -inf;
f[1][1] = g[1][1][0] = g[1][1][1] = a[1][1];
for(int j = 1; j <= m; j++) {
for(int i = 1; i <= n; i++)
f[i][j] = max(max(f[i][j], f[i][j - 1] + a[i][j]), max(g[i][j - 1][0], g[i][j - 1][1]) + a[i][j]);
for(int i = 1; i <= n; i++)
g[i][j][0] = max(max(g[i - 1][j][0], f[i - 1][j]) + a[i][j], g[i][j][0]);
for(int i = n; i; i--)
g[i][j][1] = max(max(g[i + 1][j][1], f[i + 1][j]) + a[i][j], g[i][j][1]);
}
printf("%lld\n", max(f[n][m], max(g[n][m][0], g[n][m][1])));
return 0;
}