第一题:
我永远不会忘记它那默默让题目WA的标签——双指针
由于水晶能量只增不减,所以我们可以从小到大排序,然后一一枚举可合并的水晶数量。 定义序列 \(a\) 的移动区间 \(L\) ,保证 \(a_l\) 到 \(a_r\) 的元素都可以在消耗最多 \(m\) 次操作的情况下,都合并起来。
在右端点往右移动时,由于原区间里的所有水晶都是已经被加到一样数值了,所以如果新元素加进来,设新元素为 \(a_i\) ,那么 所需的操作就为 \(a_i - a_{i - 1} \times 区间长度\) 。
那区间移动的条件还有什么?可以记录当前区间一一合并所需的操作数,如果超过了 \(m\) ,那么就将左端点往右移,因为在新加元素后, 区间里的元素是默认加到和最右元素一样的了,所以退回操作数,就为 \(a_r - a_l\) 。
代码:
#include
#define int long long
using namespace std;
int n,m,lst,ans = 1,a[500005];
signed main(){cin >> n >> m;for(int i = 1;i <= n;i++)cin >> a[i];int l = 1,r = 1;sort(a + 1,a + n + 1);while(r <= n && l <= r){while(r <= n && lst <= m){r++;lst += (r - l) * (a[r] - a[r - 1]);}ans = max(ans,r - l);while(l <= r && lst > m){lst -= a[r] - a[l];l++;}}cout<
第二题:
一句话,正难则反,注意到站厅的数量为 \(10^5\) 级别,每次查询都 Dijkstra 一遍必定会爆, 我们不妨想一下地铁线路之间的转移,毕竟题目中的 \(1\le n\le 500\) 可不是白给的。
题面里的所谓的地铁线路,其实也可以看作图上的一个点,所经过的站厅都可以步行 (即边权) 到达线路点, 同理,到达另一个地铁站也可以视作线路点与站厅之间连了一条边权为步行时间的边。
那么怎么表达换线这个概念呢?可以发现,如果一个点被多条地铁线路经过,那么这个点就可以实现换线的效果, 而单条线路不会因为走的站多还是少而影响时间,所以两个线路的转移 (即连边) 则寻找经过同一个点边权之和最小的即可。
把地铁线路变成点之后,我们就可以跑最短路,计算出两条线路之间的最短距离,因为单个节点只有最多20条线路经过, 所以完全可以枚举每次询问时两个点被经过的地铁线路,取最小值即可,记得要加到地铁口的时间。
代码:
#include
#define int long long
using namespace std;
const int inf = 1e18;
vector> G[500005];
int dis[505][505];
int c,n,m,d,q,k,w[500005];
signed main(){cin >> c >> n >> m >> d >> q;for(int i = 1;i <= m;i++)cin >> w[i];memset(dis,0x3f,sizeof(dis));for(int i = 1;i <= n;i++) dis[i][i] = 0;for(int i = 1;i <= n;i++){cin >> k;for(int j = 1;j <= k;j++){int a , t;cin >> a >> t;for(auto v : G[a]){dis[i][v.first - m] = min(dis[i][v.first - m],v.second + t);dis[v.first - m][i] = min(dis[v.first - m][i],v.second + t);}G[a].push_back({i + m,t});}}for(int k = 1;k <= n;k++){for(int i = 1;i <= n;i++){for(int j = 1;j <= n;j++)dis[i][j] = min(dis[i][j],dis[i][k] + dis[k][j]);}}while(q--){int x , y ,ans = inf;cin >> x >> y;for(auto v : G[x]){for(auto w : G[y])ans = min(ans,dis[v.first - m][w.first - m] + v.second + w.second);}cout<
第三题:
可以注意到,如果两点之间存在两条最短路径,那么必定有且只有一个两个节点的分叉,而两个节点距离的不同,也分出了不同的贡献计算。
先把图翻译成区间。坐标已经有序,所以点 \(i\) 能一步到达的点必然是一段连续的编号区间 \([l_i, r_i]\),双指针扫一遍就能求出来,而且 \(l_i\)、\(r_i\) 都单调不降,后面的结构全靠这个性质。
设两条最短路最后一次分叉于 \(a\)、第一次重新汇合于 \(b\)。能夹在中间的候选点,必须既能被 \(a\) 一步到达、又能一步到达 \(b\),也就是满足 \(l_b \le c \le r_a\),所以候选点恰好是一段连续编号 \([l_b, r_a]\),每选一个 \(c\) 就有一条 \(a \to c \to b\) 的两步路。想要恰好两条,候选点就必须恰好有两个:\(r_a - l_b + 1 = 2\),也就是 \(l_b + 1 = r_a\)。
于是两条路的中间点就是 \(q = l_b\) 和 \(p = r_a\),编号相邻——注意是编号相邻,坐标上它们可以隔得很远。中间绕更多步也救不回来:候选区间的连续性保证你能“换一个落脚点”又凑出一条,所以“恰好两条”当且仅当这个钻石成立。
接下来只需要给钻石接前缀和后缀。定义 \(f_i\) 为不断贪心往最右跳、最终会落到 \(i\) 的起点个数,\(g_i\) 完全对称,表示往最左跳会经过 \(i\) 的终点个数。\(f\) 的递推是 \(f_i = 1 + \sum_{j=l_i+1}^{i-1} [r_j = i] + [r_{l_i} = i] \cdot f_{l_i}\),最后一项别想当然:\(r_{l_i}\) 完全可能越过 \(i\),那样 \(f_{l_i}\) 里的起点会把 \(i\) 直接跳过去,不能继承。和式里的 \(j\) 同样构成一段连续区间,两个指针一起扫,\(f\)、\(g\) 都能 \(O(n)\) 求完。
最后枚举钻石。记 \(p = r_a\)、\(q = l_b\),核心就是判断两件事:\(b\) 是否等于 \(r_p\)(右边能不能继续接后缀),\(a\) 是否等于 \(l_q\)(左边能不能整体乘上 \(f_a\))。四种组合:
- \(b = r_p\) 且 \(a = l_q\):左右互不干涉,贡献 \(f_a \cdot g_b\);
- \(b = r_p\) 且 \(a > l_q\):更早的起点会在别的钻石那里被统计,这里只能接 \(u = a\),贡献 \(g_b\);
- \(b < r_p\) 且 \(a = l_q\):终点只能停在 \(b\),贡献 \(f_a\);
- \(b < r_p\) 且 \(a > l_q\):每个钻石只贡献 \((a, b)\) 这一对,开个桶存 \(l_b\),指针从左往右扫,每次查 \(cnt[r_a - 1]\)。
为什么 \(b < r_p\) 时终点不能再往右?若 \(v > b\),从 \(b\) 走出去的第一步落在 \(c \le r_b \le r_p\),那么 \(p\) 可以直接跳到 \(c\),\(a \to p \to c \to \cdots \to v\) 比原来那两条都短,钻石当场作废。而每对 \((u, v)\) 的最早分叉点和最早汇合点都是唯一的,所以三种扫描互不重叠,不用容斥。
总复杂度 \(O(N)\),\(N\) 是所有测试数据的 \(n\) 之和。答案上界是 \(\binom{n}{2}\) 量级,\(f\)、\(g\) 也一样,全开 long long;多测把数组清到 \(n\) 就够,别 memset 整个数组。
代码:
#include
#define int long long
using namespace std;
const int N = 2000005;
int n,d,x[N],l[N],r[N],f[N],g[N],cnt[N];
signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);int c,t;cin >> c >> t;while(t--){cin >> n >> d;for(int i = 1;i <= n;i++)cin >> x[i];for(int i = 1;i <= n;i++)f[i] = g[i] = cnt[i] = 0;l[1] = 1;for(int i = 2;i <= n;i++){l[i] = l[i - 1];while(x[i] - x[l[i]] > d) l[i]++;}r[n] = n;for(int i = n - 1;i >= 1;i--){r[i] = r[i + 1];while(x[r[i]] - x[i] > d) r[i]--;}int L = 0,R = 0;for(int i = 1;i <= n;i++){if(r[l[i]] == i) f[i] = f[l[i]];while(R + 1 < i && r[R + 1] <= i) R++;L = l[i] + 1;while(L < R && r[L] < i) L++;if(L <= R) f[i] += R - L + 1;f[i]++;}L = R = n;for(int i = n;i >= 1;i--){if(l[r[i]] == i) g[i] = g[r[i]];while(L - 1 > i && l[L - 1] >= i) L--;R = r[i] - 1;while(R > L && l[R] > i) R--;if(L <= R) g[i] += R - L + 1;g[i]++;}int ans = 0;for(int a = 1;a <= n;a++){int p = r[a],b = r[p],q = l[b];if(p == a || b == p || l[b] + 1 != r[a]) continue;if(l[q] == a) ans += f[a] * g[b];else if(l[q] < a) ans += g[b];}for(int b = 1;b <= n;b++){int q = l[b],a = l[q],p = r[a];if(a == q || q == b || q + 1 != p || b == r[p]) continue;if(b < r[p]) ans += f[a];}{int L = 1,R = 0;for(int a = 1;a <= n;a++){while(R + 1 < r[r[a]] && l[l[R + 1]] < a){R++;cnt[l[R]]++;}while(L <= R && L <= r[a]){cnt[l[L]]--;L++;}ans += cnt[r[a] - 1];}}cout << ans << '\n';}return 0;
}
第四题:
一句话,看到“通勤半径最小”就二分答案,把问题转成:半径固定为 \(R\) 时,最少需要几个基地。
先只考虑基地连成一块的情况。每个子树维护一个状态 \((x,c)\):
- \(c\):子树内最少放几个基地;
- 如果 \(c>0\),\(x\) 是离根最近的基地到根的距离;
- 如果 \(c=0\),\(x\) 是子树内最深未覆盖点到根的距离。
合并两个儿子分支时,四类讨论:
- 两边都是空的:\(x=\max(x_1,x_2)\)。如果最深距离够到了 \(R\),就必须在当前点放基地——再往上放就覆盖不到它了,这是最后机会,状态变成 \((0,1)\);
- 一边有基地一边空:基地到最深未覆盖点的距离是 \(x_1+x_2\),超出 \(R\) 的部分 \(x_1+x_2-R\) 要沿路径补成基地,离根最近的基地也随之上移这么多;
- 两边都有基地:两个基地块要连成一块,最近的基地之间距离 \(x_1+x_2\),中间的 \(x_1+x_2-1\) 个点全部变成基地,合并后当前点被覆盖,\(x=0\)。
然后是两个连通块。如果基地恰好分成两块,那么一定存在一条树边把它们分开:枚举每条边,两侧各按“一块”算最少基地数,取和更新答案。子树外的信息用换根求,但合并操作没有逆元,没法从整体里把某个儿子“除掉”,所以预处理儿子的前缀合并和后缀合并,去掉第 \(i\) 个儿子就是 \(pre_{i-1}\cdot suf_{i+1}\)。
“恰好 \(k\) 个”不用慌:算出至少需要 \(need\) 个,只要 \(need\le k\),就在已有基地旁边补点凑够——既不增加连通块数,也不影响半径。
复杂度 \(O(n\log n)\),但 \(\sum n\le2\times10^6\)、链上深度能到 \(10^6\),所以建树和求值全用迭代写,递归 DFS 会直接爆栈;输入也有 \(4\times10^6\) 个整数,快读顺手安排上。
代码:
#include
#define int long long
using namespace std;
const int N = 1000005;
int n,k,lim,ans,fa[N],ord[N];
bool ok[N];
vector g[N];
struct node{int x,c;node(int a = 0,int b = 0){x = a,c = b;}node operator +(int o) const{return node(x + o,c);}node operator *(const node &o) const{if(!c && !o.c){int y = max(x,o.x);if(y == lim) return node(0,1);return node(y,0);}int y = x + o.x;if(c && !o.c){if(y <= lim) return *this;return node(max(x - (y - lim),0LL),c + y - lim);}if(!c && o.c){if(y <= lim) return o;return node(max(o.x - (y - lim),0LL),o.c + y - lim);}return node(0,c + o.c + y - 1);}
};
node down[N],out[N],pre[N],suf[N];
void calcDown(){for(int i = n - 1;i >= 0;i--){int u = ord[i];down[u] = node();for(int v : g[u])if(v != fa[u]) down[u] = down[u] * (down[v] + 1);}
}
void calcUp(){for(int i = 0;i < n;i++) ok[ord[i]] = false;out[1] = node();ok[1] = true;for(int i = 0;i < n;i++){int u = ord[i];if(!ok[u] || !down[u].c) continue;int cnt = (int)g[u].size() - (u == 1 ? 0 : 1);if(!cnt) continue;pre[0] = node();int p = 0;for(int v : g[u]){if(v == fa[u]) continue;p++;pre[p] = pre[p - 1] * (down[v] + 1);}suf[cnt] = node();p = cnt;for(int j = (int)g[u].size() - 1;j >= 0;j--){int v = g[u][j];if(v == fa[u]) continue;p--;suf[p] = suf[p + 1] * (down[v] + 1);}p = 0;for(int v : g[u]){if(v == fa[u]) continue;node now = out[u] * pre[p] * suf[p + 1];if(now.c && down[v].c) ans = min(ans,now.c + down[v].c);out[v] = now + 1;ok[v] = true;p++;}}
}
int check(int R){lim = R;calcDown();ans = down[1].c;if(ans) calcUp();return ans;
}
namespace IO{
char buf[1 << 25];
int p1,p2;
inline char gc(){return p1 == p2 && (p2 = fread(buf,1,1 << 25,stdin),p1 = p2) ? 0 : buf[p1++];}
inline int read(){int x = 0;char ch = gc();while(ch < '0' || ch > '9')ch = gc();while(ch >= '0' && ch <= '9')x = x * 10 + (ch ^ 48),ch = gc();return x;}
}
signed main(){int c,t;c = IO::read(),t = IO::read();while(t--){n = IO::read(),k = IO::read();for(int i = 1;i <= n;i++) g[i].clear();for(int i = 1;i < n;i++){int u,v;u = IO::read(),v = IO::read();g[u].push_back(v);g[v].push_back(u);}if(n == k){cout << 0 << '\n';continue;}int sz = 0;ord[sz++] = 1;fa[1] = 0;for(int i = 0;i < sz;i++){int u = ord[i];for(int v : g[u])if(v != fa[u]){fa[v] = u;ord[sz++] = v;}}int l = 1,r = n / 2,best = r + 1;while(l <= r){int mid = (l + r) >> 1;if(check(mid) <= k){best = mid;r = mid - 1;}else l = mid + 1;}cout << best << '\n';}return 0;
}