2019上海區域賽現場賽總結
補題情況(以下通過率為牛客提交):
題號 | 標題 | 已通過代碼 | 通過率 | 我的狀態 |
---|---|---|---|---|
A | Mr. Panda and Dominoes | 點擊查看 | 5/29 | 未通過 |
B | Prefix Code | 點擊查看 | 249/1019 | 通過 |
C | Maze | 點擊查看 | 8/110 | 未通過 |
D | Spanning Tree Removal | 點擊查看 | 88/211 | 通過 |
E | Cave Escape | 點擊查看 | 53/553 | 通過 |
F | A Simple Problem On A Tree | 點擊查看 | 66/290 | 通過 |
G | Play the game SET | 點擊查看 | 2/35 | 未通過 |
H | Tree Partition | 點擊查看 | 73/175 | 通過 |
I | Portal | 點擊查看 | 3/3 | 未通過 |
J | Bob's Poor Math | 點擊查看 | 15/105 | 通過 |
K | Color Graph | 點擊查看 | 154/344 | 通過 |
L | Light It Down | 點擊查看 | 1/3 | 未通過 |
M | Blood Pressure Game | 點擊查看 | 3/81 | 未通過 |
B
可以用字典樹,但實際unordered_map就可以了,更好寫一點
D
這個題比較搞,但其實想出來的話特別好寫(現場賽做崩了,298 1A)
題意是給一個 n (\(n\le 1000\)) 個點的完全圖 , 每次刪除一個n結點的生成樹(在原圖刪除對應的邊),問最多可以刪除多少次。
容易想到 n 個點的完全圖總邊數是 \(n*(n-1) / 2\) ,而一個生成樹的邊數為 \((n-1)\) ,所以最多可以構造出\(n/2\) 棵生成樹。
n 為偶數時,枚舉\((x, y),x=2k-1,y=2k,k\in[1,n/2]\) , 將 \(x\) 與小於\(x\) 的偶數相連,與大於\(x\) 的奇數相連、將\(y\) 與小於 \(y\) 的奇數相連,與大於\(y\) 的偶數相連。
如此構造可以使得:
- 每個偶數都與比它大的偶數和比它小的奇數相連,都被比它小的偶數和比它大的奇數相連
- 每個奇數都與比它小的偶數和比它大的奇數相連,都被比它大的偶數和比它小的奇數相連
可以發現,每個點都只與其他\((n-1)\) 個點搭配了一次。
對於 n 為奇數的情況,先按照\((n-1)\) 為偶數的情況去處理,然后在第 \(i\) 個 \((n-1)\)結點的生成樹中,將\((i,n)\)相連即可。
int T, n, cas;
void get(int s, int t, int n){
for (int i = 2; i < s;i += 2)
printf("%d %d\n", i, s);
for (int i = s + 2; i <= n;i += 2)
printf("%d %d\n", s, i);
for (int i = 1; i < t; i += 2)
printf("%d %d\n", i, t);
for (int i = t + 2; i <= n;i+=2)
printf("%d %d\n", t, i);
}
int main() {
scanf("%d", &T);
while(T--){
scanf("%d", &n);
printf("Case #%d: %d\n", ++cas, n / 2);
for (int i = 1; i < n;i+=2){
if(n & 1)
printf("%d %d\n", i, n);
get(i, i + 1, n - (n & 1));
}
}
return 0;
}
關於怎么想到這么一種構造方法...首先只考慮n為偶數的情況,然后每次枚舉兩個點,做為整個樹的“中心樞紐", 其他點各分一半與這兩個點相連,按照規律去找。
E
比較裸的最大生成樹,如果不是多組數據就可以裸着做了。
T = 10,點數1e6, 邊數4e6級別。對邊排序或者堆優化的prim都不太靠得住,但是注意下題目數據就會發現,邊權最大只可能是10000,所以按照桶排的思想去處理邊即可。然后kruskal。
const int N = 1000 + 5;
typedef pair<int, int> pii;
int T,n,m;
pii st, ed;
int val[N][N];
int x[1000010],A, B, C, P;
int id[N][N], vis[N][N], fa[N*N];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
vector<pii> edge[10010];
int main()
{
scanf("%d", &T);
int cas = 0;
while(T--){
scanf("%d%d%d%d%d%d", &n, &m, &st.first, &st.second, &ed.first, &ed.second);
scanf("%d%d%d%d%d%d", &x[1], &x[2], &A, &B, &C, &P);
for (int i = 1; i <= n*m;i++)
fa[i] = i;
for (int i = 0; i <= 10000;i++)
edge[i].clear();
for (int i = 3; i <= n * m; i++)
{
x[i] = (A * x[i - 1] + B * x[i - 2] + C) % P;
}
int k = 0;
for (int i = 1; i <= n;i++)
for (int j = 1; j <= m;j++){
id[i][j] = ++k;
val[i][j] = x[k];
if(i > 1)
edge[val[i][j] * val[i - 1][j]].push_back({ id[i][j], id[i - 1][j] });
if(j > 1)
edge[val[i][j] * val[i][j - 1]].push_back({ id[i][j], id[i][j - 1] });
}
ll res = 0, cnt = 0;
for (int i = 10000; i >= 0;i--){
for (int j = 0; j < edge[i].size();j++){
int x = edge[i][j].first, y = edge[i][j].second;
if(find(x) == find(y))
continue;
res += i;
fa[find(x)] = find(y);
cnt++;
if(cnt == k-1)
break;
}
if(cnt == k-1)
break;
}
printf("Case #%d: %lld\n", ++cas, res);
}
return 0;
}
F
顯然是樹剖,不過要維護的東西有那么一點惡心人。
維護\(\sum w^3\) ,從三個操作去分解它,得到線段樹中需要存放的東西
- 區間賦值為 w
- 區間加 w
- 區間乘 w
為了簡化線段樹的操作,發現1號操作等價於先乘一個0,在加一個 w。所以這三個操作都可以轉換為這個形式:區間先乘一個數 x,再加一個數 y
要維護 \(\sum w^3\) ,乘的操作看起來比較容易,所以先來看看加一個數如何維護
\((w+y)^3 = w^3 + y^3 + 3*w^2*y + 3*w*y^2\)
\(\Rightarrow \sum (x+y)^3 = \sum w^3 + \sum y^3 + 3*\sum w^2*y + 3*\sum w* y^2\)
發現線段樹中需要維護三個值:\(\sum w^3, \sum w^2, \sum w\)
這三個值分別用 \(s3, s2, s\) 表示
對於乘法更新:
對於加法更新(注意變量更新順序):
lazy_tag 更新:
由於絕大部分是板子,所以代碼列出主要部分, 下面是線段樹的操作代碼:
#define ls (p*2)
#define rs (p*2+1)
struct SegTree{
int l, r;
ll len, s, s2, s3, mul, add;
} t[N << 2];
void pushup(int p){
t[p].s = (t[ls].s + t[rs].s) % mod;
t[p].s2 = (t[ls].s2 + t[rs].s2) % mod;
t[p].s3 = (t[ls].s3 + t[rs].s3) % mod;
}
void build(int p,int l,int r){
t[p].l = l, t[p].r = r;
t[p].mul = 1;
t[p].add = t[p].s = t[p].s2 = t[p].s3 = 0;
t[p].len = r - l + 1;
if(l == r){
ll val = w[rnk[l]];
t[p].s = val;
t[p].s2 = val * val % mod;
t[p].s3 = val * val % mod * val % mod;
return;
}
int mid = l + r >> 1;
build(p * 2, l, mid);
build(p * 2 + 1, mid + 1, r);
pushup(p);
}
ll S3(ll x) { return x * x % mod * x % mod; }
ll S2(ll x) { return x * x % mod; }
void calc(int p,ll x, ll y){ // 這里千萬要細心
//乘法更新
t[p].s = t[p].s * x % mod;
t[p].s2 = t[p].s2 * S2(x) % mod;
t[p].s3 = t[p].s3 * S3(x) % mod;
//加法更新,這里要按照s3,s2,s的順序依次更新
t[p].s3 = t[p].s3 + S3(y) * t[p].len % mod + 3ll * t[p].s2 * y % mod + 3ll * t[p].s * S2(y) % mod;
t[p].s3 %= mod;
t[p].s2 = (t[p].s2 + 2ll * t[p].s * y % mod + S2(y)*t[p].len) % mod;
t[p].s = (t[p].s + y * t[p].len % mod) % mod;
//最后也不能掉以輕心
t[p].mul = t[p].mul * x % mod;
t[p].add = (t[p].add * x % mod + y) % mod;
}
void pushdown(int p){
if(t[p].mul != 1 || t[p].add){
calc(ls, t[p].mul, t[p].add);
calc(rs, t[p].mul, t[p].add);
t[p].mul = 1, t[p].add = 0;
}
}
void change(int p,int l,int r,ll x,ll y){
if(t[p].l >= l && t[p].r <= r){
calc(p, x, y);
return;
}
pushdown(p);
int mid = t[p].l + t[p].r >> 1;
if(mid >= l)
change(ls, l, r, x, y);
if(mid < r)
change(rs, l, r, x, y);
pushup(p);
}
ll query(int p,int l,int r){
if(t[p].l >= l && t[p].r <= r)
return t[p].s3;
pushdown(p);
int mid = t[p].l + t[p].r >> 1;
ll res = 0;
if(mid >= l)
res += query(ls, l, r);
if(mid < r)
res += query(rs, l, r);
return res % mod;
}
H
二分+樹形DP
枚舉最小值mid,按照這個值對樹進行分塊。
對於 \(x\) 結點,遞歸處理它的子節點,並將所有子節點的剩余價值放到一個容器中,進行排序,優先考慮小的。不能裝的時候,后面的要全部割掉。
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 100000 + 5;
int n, k, cnt, head[N], ver[N<<1], nxt[N<<1], tot;
ll w[N], d[N], lim;
void add(int x,int y){
ver[++tot] = y, nxt[tot] = head[x], head[x] = tot;
}
void dfs(int x,int fa){
vector<ll> val;//放在內部還是外部需要仔細考慮,如果貪戀時間,可以放外面,不過要注意要先dfs所有子節點后,再掃一遍將他們加入到序列中,函數返回時要記得clear
for (int i = head[x]; i;i=nxt[i]){
int y = ver[i];
if(y == fa)
continue;
dfs(y, x);
val.push_back(d[y]);
}
sort(val.begin(), val.end());
d[x] = w[x];
for (int i = 0; i < val.size();i++){
if(d[x] + val[i] > lim){
cnt += val.size() - i;
break;
}
d[x] += val[i];
}
}
bool check(ll mid){
lim = mid;
cnt = 1;
dfs(1, 0);
return cnt <= k;
}
int main() {
int T, cas = 0;
scanf("%d", &T);
while(T--){
scanf("%d%d", &n, &k);
tot = 0;
for (int i = 1; i <= n;i++)
head[i] = 0;
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d%d", &x, &y);
add(x, y);
add(y, x);
}
ll l = 1, r = 0;
for (int i = 1; i <= n;i++){
scanf("%lld", &w[i]);
r += w[i];
l = max(l, w[i]);
}
while(l < r){
ll mid = l + r >> 1;
if(check(mid))
r = mid;
else
l = mid + 1;
}
printf("Case #%d: %lld\n", ++cas, l);
}
return 0;
}
J
字典樹,類似線段樹的操作,細節挺多
- Add 操作,直接在字典樹中加一個新的數字
- Shift操作,new一個新的根,將原根接到新根的0號位置,並更新sum值,打tag標記
- Query操作,每一層通過枚舉找到一個最大可以組合成的數字
- Sum 操作,類似線段樹的區間求和
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 200000 + 5;
int tr[N][10], n, m, rt, tot;
ll sum[N], tag[N], l, r, x, p[11];
char op[10];
void clear(){
for (int i = 1; i <= tot;i++){
memset(tr[i], 0, sizeof tr[i]);
sum[i] = tag[i] = 0;
}
tot = rt = 1;
}
ll add_val(ll x, ll y){//按照題意的加法原則進行相加
if(x == 0 || y == 0)
return x | y;
ll res = 0;
for (int i = 0; i < 10;i++, x /= 10, y /= 10){
res += (x % 10 + y % 10) % 10 * p[i];
}
return res;
}
void pushdown(int u){
if(tag[u] == 0)
return;
for(int i = 0; i <= 9;i++){
if(!tr[u][i])
continue;
int v = tr[u][i];
if(tag[u] > 10)
sum[v] = 0;
else
sum[v] /= p[tag[u]], tag[v] += tag[u];
}
tag[u] = 0;
}
void add(int u,int x){
sum[u] = add_val(sum[u], x);
for (int i = 9; i >= 0;i--){
pushdown(u);
int c = x / p[i] % 10;
if(tr[u][c] == 0)
tr[u][c] = ++tot;
u = tr[u][c];
sum[u] = add_val(sum[u], x);
}
}
void shift(){
tr[++tot][0] = rt;
sum[tot] = sum[rt] / 10;
tag[rt = tot]++;
}
ll query_sum(ll x){
ll res = 0;
int u = rt;
for (int i = 9; i >= 0;i--){
pushdown(u);
int c = x / p[i] % 10, j = 9;
//找到最大的可組合的 j
while(tr[u][(j+10-c)%10] == 0)
j--;
res += j * p[i];
u = tr[u][(j + 10 - c) % 10];
}
return res;
}
ll get(int dep, int u, ll l, ll r, ll x, ll y){
if(y < l || x > r)
return 0;
if(l >= x && r <= y){
return sum[u];
}
pushdown(u);
ll res = 0;
for (int i = 0; i <= 9;i++){
if(!tr[u][i])
continue;
//注意這里的[nl,nr]的計算
ll nl = l + p[dep] * i, nr = l + p[dep] * (i + 1) - 1;
res = add_val(res, get(dep - 1, tr[u][i], nl, nr, x, y));
}
return res;
}
int main() {
p[0] = 1;
for (int i = 1; i <= 10;i++)
p[i] = p[i - 1] * 10;
int T, cas = 0;
scanf("%d", &T);
while(T--){
scanf("%d%d", &n, &m);
clear();
for (int i = 1; i <= n;i++){
scanf("%lld", &x);
add(rt, x);
}
printf("Case #%d:\n", ++cas);
while(m--){
scanf("%s", op);
if(op[0] == 'S' && op[1] == 'h')
shift();
else if(op[0] == 'A'){
scanf("%lld", &x);
add(rt, x);
}else if(op[0] == 'Q'){
scanf("%lld", &x);
printf("%lld\n", query_sum(x));
}else if(op[0] == 'S'){
scanf("%lld%lld", &l, &r);
printf("%lld\n", get(9, rt, 0, p[10] - 1, l, r));
}
}
}
return 0;
}
K
染邊不存在奇環,那么染邊與點構成一個二分圖,枚舉每個點在這個二分圖中的所屬,將所有邊進行枚舉,如果該邊的兩個點在二分圖中所屬不同,那么貢獻++
const int N = 16 + 5;
int T, n, m, st[N];
int cas;
pair<int, int> e[N * N];
int main() {
scanf("%d", &T);
while(T--){
scanf("%d%d", &n, &m);
for (int i = 1; i <= m;i++){
scanf("%d%d", &e[i].first, &e[i].second);
}
int res = 0;
for (int i = 0; i < (1 << n);i++){
for (int j = 0; j < n;j++){
if(i >> j & 1)
st[j + 1] = 1;
else
st[j + 1] = 0;
}
int cur = 0;
for (int i = 1; i <= m;i++){
if(st[e[i].first] != st[e[i].second]){
cur++;
}
}
res = max(res, cur);
}
printf("Case #%d: %d\n", ++cas, res);
}
return 0;
}