第十六届上海大学程序设计联赛春季赛暨上海高校金马五校赛 题解
【题目链接】
模拟。从左往右填充每一个,如果某一个格子不足,需要从右边离他最近的有盈余的格子里拿一些来填充;如果某一个格子有盈余,那么多余部分往右扔过去。
/******************************* Judge Result : AC *******************************/ #include <bits/stdc++.h> using namespace std; const int maxn = 1e5 + 10; const int INF = 0x7FFFFFFF; int T, n; long long a[maxn], b[maxn]; int main() { #ifdef ZHOUZHENTAO freopen("test.in", "r", stdin); #endif scanf("%d", &T); while(T --) { scanf("%d", &n); for(int i = 1; i <= n; i ++) { scanf("%lld", &a[i]); } for(int i = 1; i <= n; i ++) { scanf("%lld", &b[i]); a[i] = a[i] - b[i]; } long long ans = 0; int p = 1; for(int i = 1; i <= n; i ++) { if(a[i] == 0) continue; if(a[i] > 0) ans += a[i], a[i + 1] += a[i], a[i] = 0; else { while(1) { while(a[p] <= 0) p ++; if(a[i] + a[p] >= 0) { ans = ans + (p - i) * (-a[i]); a[p] += a[i]; a[i] = 0; break; } else { a[i] = a[i] + a[p]; ans = ans + (p - i) * a[p]; a[p] = 0; } } } } printf("%lld\n", ans); } return 0; }
B - 合约数
由于是处理子树问题,所以可以将树转成 dfs 序,然后就变成了区间问题。然后就是询问区间上有几个合约数,莫队操作一下就可以了。
#include <cstdio> #include <cmath> #include <cstring> #include <string> #include <vector> #include <algorithm> using namespace std; #define LL long long const int maxn = 2e5 + 10; vector<int> g[maxn]; int pri[maxn]; int T, n, root; int a[maxn], sz; int L[maxn], R[maxn]; int h[maxn], nx[maxn], to[maxn], cnt; int val[maxn], pos[maxn]; int b[maxn], f[maxn]; struct point { int id, l, r; }s[maxn]; bool cmp(const point& a, const point& b) { if(pos[a.l] == pos[b.l]) return a.r < b.r; return pos[a.l] < pos[b.l]; } int prime(int x) { if(x == 1) return 0; for(int i = 2; i * i <= x; i ++) { if(x % i == 0) return 0; } return 1; } void init() { for(int i = 1; i <= 10000; i ++) { pri[i] = prime(i); } for(int i = 4; i <= 10000; i ++) { if(pri[i]) continue; for(int j = 4; j <= i; j ++) { if(i % j) continue; if(pri[j]) continue; g[i].push_back(j); } } } void dfs(int x, int fa) { sz ++; a[sz] = x; L[x] = sz; for(int i = h[x]; i != -1; i = nx[i]) { if(to[i] == fa) continue; dfs(to[i], x); } R[x] = sz; } void add(int x, int y) { to[cnt] = y; nx[cnt] = h[x]; h[x] = cnt ++; } void Delete(int x) { b[val[a[x]]] --; } void Insert(int x) { b[val[a[x]]] ++; } int main() { init(); scanf("%d", &T); while(T --) { scanf("%d %d", &n, &root); int sqr = (int)sqrt(1.0 * n); for(int i = 1; i <= n; i ++) { pos[i] = i / sqr; } for(int i = 1; i <= 10000; i ++) { b[i] = 0; } cnt = 0; for(int i = 1; i <= n;i ++) { h[i] = -1; } for(int i = 0; i < n - 1; i ++) { int x, y; scanf("%d %d", &x, &y); add(x, y); add(y, x); } sz = 0; dfs(root, -1); /* for(int i = 1; i <= n; i ++) { printf("%d ", a[i]); } printf("\n"); for(int i = 1; i <= n; i ++) { printf("!!! %d %d %d\n", i, L[i], R[i]); } printf("ok\n"); */ for(int i = 1; i <= n; i ++) { scanf("%d", &val[i]); } for(int i = 1; i <= n; i ++) { s[i].id = i; s[i].l = L[i]; s[i].r = R[i]; } sort(s + 1, s + 1 + n, cmp); /* for(int i = 1; i <= n; i ++) { printf("q : %d %d %d\n", s[i].id, s[i].l, s[i].r); } */ for(int i = s[1].l; i <= s[1].r; i ++) { Insert(i); } f[s[1].id] = 0; for(int j = 0; j < g[val[s[1].id]].size(); j ++) { f[s[1].id] = f[s[1].id] + b[g[val[s[1].id]][j]]; } int left = s[1].l, right = s[1].r; for(int i = 2; i <= n; i ++) { while(left > s[i].l) left --, Insert(left); while(right < s[i].r) right ++, Insert(right); while(left < s[i].l) Delete(left), left ++; while(right > s[i].r) Delete(right), right --; f[s[i].id] = 0; for(int j = 0; j < g[val[s[i].id]].size(); j ++) { f[s[i].id] = f[s[i].id] + b[g[val[s[i].id]][j]]; } } /* for(int i = 1; i <= n; i ++) { printf("!!! %d : %d\n", i, f[i]); } */ long long mod = 1e9 + 7; long long ans = 0; for(int i = 1; i <= n; i ++) { long long tmp = 1LL * i * f[i] % mod; ans = (ans + tmp) % mod; } printf("%lld\n", ans); } return 0; } /* 100 13 10 10 2 3 1 4 11 4 12 4 13 10 4 2 5 10 3 3 8 3 9 2 6 2 7 30 60 50 24 5 10 12 25 10 120 2 3 4 */
C - 序列变换
枚举全排列,计算每一种排列需要的操作次数。从一个排列转换成另一个排列,可以模拟搞。从一个数字转换成另一个数字,只能选择一种方式,因此很简单。
/******************************* Judge Result : AC *******************************/ #include <bits/stdc++.h> using namespace std; const int maxn = 1e5 + 10; const int INF = 0x7FFFFFFF; int T, n; int a[maxn], b[maxn]; int cost[20][20]; int p[maxn]; int u[maxn]; int f[maxn]; int work(int x, int y) { if(x == y) return 0; if(x < y) swap(x, y); int sum = 0; while(1) { if(x % 2 == 0) { if(x / 2 >= y) sum ++, x = x / 2; else return sum + x - y; } else { if(x == y) return sum; else sum ++, x --; } } return sum; } int get() { int sum = 0; for(int i = 1; i <= n; i ++) { u[i] = p[i]; } for(int i = 1; i <= n; i ++) { if(u[i] == i) continue; sum ++; for(int j = i + 1; j <= n; j ++) { if(u[j] == i) { swap(u[i], u[j]); break; } } } return sum; } int main() { #ifdef ZHOUZHENTAO freopen("test.in", "r", stdin); #endif scanf("%d", &T); while(T --) { scanf("%d", &n); for(int i = 1; i <= n; i ++) { scanf("%d", &a[i]); } for(int i = 1; i <= n; i ++) { scanf("%d", &b[i]); } for(int i = 1; i <= n; i ++) { for(int j = 1; j <= n; j ++) { cost[i][j] = work(a[i], b[j]); //cout << a[i] << " -> " << b[j] << " : " << cost[i][j] << endl; } } for(int i = 1; i <= n; i ++) { p[i] = i; } int ans = 2e9; do { int tmp = get(); for(int i = 1; i <= n; i ++) { tmp = tmp + cost[p[i]][i]; } ans = min(ans, tmp); } while(next_permutation(p + 1, p + 1 + n)); printf("%d\n", ans); } return 0; }
D - 数字游戏
大致思路为如果无论 $n_1$ 为多少,都有一个 $n_2$ 能够找到,使得结果是 mod 的倍数,则先手输;否则后手输。由于 mod 范围不大,所以可以枚举一下 $n_2$ 的位数,然后暴力枚举处理,只要枚举 mod 范围内即可,因为取模之后的数不会超过 mod。
E - 小Y吃苹果
答案是 2 的 $n$ 次方。
#include <stdio.h> int main() { int n; scanf("%d", &n); printf("%d\n", 1 << n); return 0; }
F - 1 + 2 = 3?
找找规律可以发现满足条件的数字是二进制上没有相邻的 1。因此可以二分答案,然后数位 dp 计算方案数。
#include <iostream> #include <cstdio> #include <cmath> #include <cstring> #include <queue> using namespace std; #define LL long long LL dp[110][2]; LL a[110]; LL n; LL dfs(int len,int sta,bool limit) { if(len<0) return 1; if(dp[len][sta]!=-1&&!limit) return dp[len][sta]; int up=limit?a[len]:1; LL ans=0; for(int i=0; i<=up; i++) { if(sta&&i==1) continue; ans+=dfs(len-1,i==1,limit&&i==up); } return limit?ans:dp[len][sta]=ans; } LL solve(LL x) { memset(dp,-1,sizeof dp); int cnt=0; while(x>0) { a[cnt++]=x%2; x/=2; } return dfs(cnt-1,0,1); } int main() { int T; scanf("%d", &T); while(T--) { scanf("%lld", &n); LL l = 1, r = 1e18; LL mid, ans; while(l <= r) { mid = (l + r) / 2; LL t = solve(mid) - 1; if(t < n) { l = mid + 1; } else { ans = mid; r = mid - 1; } } printf("%lld\n", ans); } return 0; }
G - 小Y做比赛
暂时不会做
H - 小Y与多米诺骨牌
先处理出选择 $i$ 位置往左倒,最左会使得 $L_i$ 也到下;往右倒,最右会使得 $R_i$ 也倒下。
然后进行 dp,$dp_i$ 表示 $[1, i]$ 都倒下需要的最少操作次数。有两种途径,一种是 $[1, j]$ 先倒下,然后选择 $i$ 往左倒,使得 $[j+1,i]$ 都倒下;另一种是 $[1, j]$ 先倒下,然后选择 $j$ 向右倒,使得 $[j+1,i]$ 都倒下。两种都可以用线段树来维护来得到最优解。
/******************************* Judge Result : *******************************/ #include <bits/stdc++.h> using namespace std; const int maxn = 1e5 + 10; const int INF = 1e6; int x[maxn], y[maxn]; int T, n; int L[maxn], R[maxn]; int s[2][4 * maxn]; int dp[maxn]; vector<int> g[maxn]; void build(int flag, int val, int l, int r, int rt) { s[flag][rt] = val; if(l == r) return ; int mid = (l + r) / 2; build(flag, val, l, mid, 2 * rt); build(flag, val, mid + 1, r, 2 * rt + 1); } void update(int flag, int op, int pos, int val, int l, int r, int rt) { if(l == r) { s[flag][rt] = val; return; } int mid = (l + r) / 2; if(pos <= mid) update(flag, op, pos, val, l, mid, 2 * rt); else update(flag, op, pos, val, mid + 1, r, 2 * rt + 1); if(op == 0) s[flag][rt] = min(s[flag][2 * rt], s[flag][2 * rt + 1]); else s[flag][rt] = max(s[flag][2 * rt], s[flag][2 * rt + 1]); } int getmin(int flag, int L, int R, int l, int r, int rt) { if(L <= l && r <= R) { return s[flag][rt]; } int mid = (l + r) / 2; int left = INF, right = INF; if(L <= mid) left = getmin(flag, L, R, l, mid, 2 * rt); if(R > mid) right = getmin(flag, L, R, mid + 1, r, 2 * rt + 1); return min(left, right); } int getmax(int flag, int L, int R, int l, int r, int rt) { if(L <= l && r <= R) { return s[flag][rt]; } int mid = (l + r) / 2; int left = 0, right = 0; if(L <= mid) left = getmax(flag, L, R, l, mid, 2 * rt); if(R > mid) right = getmax(flag, L, R, mid + 1, r, 2 * rt + 1); return max(left, right); } int main() { #ifdef ZHOUZHENTAO freopen("test.in", "r", stdin); #endif scanf("%d", &T); while(T --) { scanf("%d", &n); for(int i = 1; i <= n; i ++) { scanf("%d%d", &x[i], &y[i]); } build(0, INF, 1, n, 1); L[1] = 1; update(0, 0, 1, 1, 1, n, 1); for(int i = 2; i <= n; i ++) { update(0, 0, i, i, 1, n, 1); int left = 1, right = i, ans = -1; while(left <= right) { int mid = (left + right) / 2; if(x[mid] > x[i] - y[i]) ans = mid, right = mid - 1; else left = mid + 1; } L[i] = getmin(0, ans, i, 1, n, 1); update(0, 0, i, L[i], 1, n, 1); } build(0, 0, 1, n, 1); R[n] = n; update(0, 1, n, n, 1, n, 1); for(int i = n - 1; i >= 1; i --) { update(0, 1, i, i, 1, n, 1); int left = i, right = n, ans = -1; while(left <= right) { int mid = (left + right) / 2; if(x[mid] < x[i] + y[i]) ans = mid, left = mid + 1; else right = mid - 1; } R[i] = getmax(0, i, ans, 1, n, 1); update(0, 1, i, R[i], 1, n, 1); } /* for(int i = 1; i <= n; i ++) { cout << i << " " << L[i] << " " << R[i] << endl; } */ for(int i = 1; i <= n; i ++) { g[i].clear(); } for(int i = 1; i <= n; i ++) { g[R[i]].push_back(i); } build(0, INF, 0, n, 1); build(1, INF, 0, n, 1); update(0, 0, 0, 0, 0, n, 1); update(1, 0, 0, 0, 0, n, 1); for(int i = 1; i <= n; i ++) { dp[i] = getmin(1, L[i] - 1, i - 1, 0, n, 1) + 1; dp[i] = min(dp[i], getmin(0, 0, i - 1, 0, n, 1) + 1); update(1, 0, i, dp[i], 0, n, 1); update(0, 0, i, dp[i], 0, n, 1); for(int j = 0; j < g[i].size(); j ++) { update(0, 0, g[i][j] - 1, INF, 0, n, 1); } } /* for(int i = 1; i <= n ; i ++) { printf("dp[%d] = %d\n", i, dp[i]); } */ printf("%d\n", dp[n]); } return 0; }
I - 二数
构造一下最大的小于等于 $n$ 的二数,以及最小的大于等于 $n$ 的二数,取小的就是答案。
#include <cstdio> #include <cmath> #include <cstring> #include <string> using namespace std; #define LL long long char s[1000111]; int main() { int T; scanf("%d", &T); while (~scanf("%s", &s)) { int k=strlen(s); if(s[0]=='1'&&k==1){ printf("0\n"); continue; } int flag=0; for(int i=0;i<k;i++){ if((s[i]-'0')%2==1){ if(s[i]=='9'){ flag=-1; break; } for(int j=i+1;j<k;j++){ if(s[j]>'4'){ flag=1; break; } else if(s[j]<'4'){ flag=-1; break; } } if(flag!=0) break; flag=-1; break; } } if(flag==0){ printf("%s",s); } else if(flag==-1){ int i=0; for(i=0;i<k;i++){ if((s[i]-'0')%2==1){ if(s[i]>'1'||i>0) printf("%c",s[i]-1); break; } printf("%c",s[i]); } i++; for(;i<k;i++){ printf("%c",'8'); } } else if(flag==1){ int i=0; for(i=0;i<k;i++){ if((s[i]-'0')%2==1){ printf("%c",s[i]+1); break; } printf("%c",s[i]); } i++; for(;i<k;i++){ printf("%c",'0'); } } printf("\n"); } return 0; }
J - 小Y写文章
二分答案 $x$,需要验证答案小于等于 $x$ 能否做到。验证:可以考虑为有 $n + 1$ 个空位,需要 $m$ 个物品去填充。如果某个物品可以放在某个位置,就连边,容量为 1。需要额外增加一个节点,如果某个位置可以不放物品,这个节点就和那个位置连边。看看网络最大流是不是为 $n+1$ 即可。如果是,则说明存在放置的方案。
/******************************* Judge Result : AC *******************************/ #include <bits/stdc++.h> using namespace std; const int maxn = 500 + 10; const int INF = 0x7FFFFFFF; struct Edge { int from, to, cap, flow; Edge(int u, int v, int c, int f) :from(u), to(v), cap(c), flow(f){} }; vector<Edge>edges; vector<int>G[maxn]; bool vis[maxn]; int d[maxn]; int cur[maxn]; int S, T; void init() { for (int i = 0; i < maxn; i++) G[i].clear(); edges.clear(); } void AddEdge(int from, int to, int cap) { // cout << from << " -> " << to << " " << cap << endl; edges.push_back(Edge(from, to, cap, 0)); edges.push_back(Edge(to, from, 0, 0)); int w = edges.size(); G[from].push_back(w - 2); G[to].push_back(w - 1); } bool BFS() { memset(vis, 0, sizeof(vis)); queue<int>Q; Q.push(S); d[S] = 0; vis[S] = 1; while (!Q.empty()) { int x = Q.front(); Q.pop(); for (int i = 0; i<G[x].size(); i++) { Edge e = edges[G[x][i]]; if (!vis[e.to] && e.cap>e.flow) { vis[e.to] = 1; d[e.to] = d[x] + 1; Q.push(e.to); } } } return vis[T]; } int DFS(int x, int a) { if (x == T || a == 0) return a; int flow = 0, f; for (int &i = cur[x]; i<G[x].size(); i++) { Edge e = edges[G[x][i]]; if (d[x]+1 == d[e.to]&&(f=DFS(e.to,min(a,e.cap-e.flow)))>0) { edges[G[x][i]].flow+=f; edges[G[x][i] ^ 1].flow-=f; flow+=f; a-=f; if(a==0) break; } } if(!flow) d[x] = -1; return flow; } int dinic(int s, int t) { int flow = 0; while (BFS()) { memset(cur, 0, sizeof(cur)); flow += DFS(s, INF); } return flow; } int a[maxn], b[maxn]; int n, m; int check(int limit) { init(); int s = 0; int t = n + m + 2; S = n + m + 3; T = n + m + 4; AddEdge(S, s, INF); AddEdge(t, T, INF); AddEdge(n + m + 5, 0 + 1 + m, 1); AddEdge(n + m + 5, n + 1 + m, 1); for(int i = 1; i < n ; i ++) { if(abs(a[i] - a[i + 1]) <= limit) { AddEdge(n + m + 5, i + 1 + m, 1); } } AddEdge(s, n + m + 5, n + 1 - m); for(int i = 1; i <= m; i ++) { AddEdge(s, i, 1); } for(int i = 1; i <= m; i ++) { for(int j = 0; j <= n; j ++) { if(j == 0) { if(abs(b[i] - a[1]) <= limit) { AddEdge(i, j + 1 + m, 1); } } else if(j < n) { if(abs(b[i] - a[j]) <= limit && abs(b[i] - a[j + 1]) <= limit) { // cout << i << " " << j << endl; AddEdge(i, j + 1 + m, 1); } } else { if(abs(b[i] - a[n]) <= limit) { AddEdge(i, j + 1 + m, 1); } } } } for(int j = 0; j <= n; j ++) { AddEdge(j + 1 + m, t, 1); } if(dinic(S, T) == n + 1) return 1; return 0; } int main() { #ifdef ZHOUZHENTAO freopen("test.in", "r", stdin); #endif int T; scanf("%d", &T); while(T --) { scanf("%d%d", &n, &m); for(int i = 1; i <= n; i ++) { scanf("%d", &a[i]); } for(int i = 1; i <= m; i ++) { scanf("%d", &b[i]); } int left = 0, right = 2e9, ans; while(left <= right) { int mid = (left + right) / 2; if(check(mid)) ans = mid, right = mid - 1; else left = mid + 1; } printf("%d\n", ans); } return 0; }
K - 树上最大值
有个东西叫线性基:支持插入元素,合并两个集合,计算集合中选一些数异或值最大的方法。所有操作复杂度均为 $O(log(value))$。有了这个方法这题就很简单了。
枚举所有子树,A 集合在这个子树中,B 集合在子树以外部分。因此可以转化为区间问题,子树内用树形dp加上上面那个方法做就好了,子树外就是两个区间的并集,用前缀和后缀的并集即可。
/******************************* Judge Result : AC *******************************/ #include <bits/stdc++.h> using namespace std; const int maxn = 1e5 + 2; const int INF = 0x7FFFFFFF; struct Base{ int a[30]; void init(){for(int i=0;i<30;i++)a[i]=0;} void up(int &a, int b) {if(b>a)a=b;} void ins(int x){for(int i=29;~i;i--)if(x>>i&1){if(a[i])x^=a[i];else{a[i]=x;break;}}} int ask(){ int t=0; for(int i=29;~i;i--)up(t,t^a[i]); return t; } }; Base merge(const Base& b1, const Base& b2) { Base res = b1; for(int i = 0; i < 30; i ++) { if(b2.a[i]) res.ins(b2.a[i]); } return res; } int A[maxn], sz; int L[maxn], R[maxn]; int h[maxn], nx[2 * maxn], to[2 * maxn], cnt; int n, val[maxn]; Base node[maxn], pre[maxn], suf[maxn]; void dfs(int x, int fa) { sz ++; A[sz] = x; L[x] = sz; node[x].init(); node[x].ins(val[x]); for(int i = h[x]; i != -1; i = nx[i]) { if(to[i] == fa) continue; dfs(to[i], x); node[x] = merge(node[x], node[to[i]]); } R[x] = sz; } void add(int x, int y) { to[cnt] = y; nx[cnt] = h[x]; h[x] = cnt ++; } int main() { #ifdef ZHOUZHENTAO freopen("test.in", "r", stdin); #endif scanf("%d", &n); for(int i = 1; i <= n; i ++) { h[i] = -1; scanf("%d", &val[i]); } for(int i = 0; i < n - 1; i ++) { int x, y; scanf("%d %d", &x, &y); add(x, y); add(y, x); } dfs(1, -1); pre[0].init(); for(int i = 1; i <= n; i ++) { pre[i] = pre[i - 1]; pre[i].ins(val[A[i]]); } suf[n + 1].init(); for(int i = n; i >= 1; i --) { suf[i] = suf[i + 1]; suf[i].ins(val[A[i]]); } int ans = 0; for(int i = 1; i <= n; i ++) { Base tmp = node[i]; Base left, right; left.init(); right.init(); if(L[i] != 1) left = pre[L[i] - 1]; if(R[i] != n) right = suf[R[i] + 1]; ans = max(ans, tmp.ask() + merge(left, right).ask()); } printf("%d\n", ans); return 0; }
L - K序列
$dp_{i,j}$ 表示前 $i$ 个数,和对 $mod$ 取模的结果为 $j$ 的子序列的最长长度。开个滚动数组就好了。
#include <cstdio> #include <cmath> #include <cstring> #include <string> using namespace std; #define LL long long int num[100111]; int dp[2][10001111]; int n, k; int main() { while (~scanf("%d %d", &n, &k)) { for (int i = 0; i < n; ++ i) { scanf("%d", num + i); num[i] %= k; } int wei = 0; memset(dp, -1, sizeof dp); dp[wei][0] = 0; for (int i = 0; i < n; ++ i) { for (int j = 0; j < k; ++ j) { dp[wei ^ 1][(j + num[i]) % k] = dp[wei][(j + num[i]) % k]; if (dp[wei][j] != -1) dp[wei ^ 1][(j + num[i]) % k] = max(dp[wei][(j + num[i]) % k], dp[wei][j] + 1); } wei ^= 1; } printf("%d\n", dp[wei][0]); } return 0; }