|
真(ru)的(yi)是(ke)签(ai)到(miao)。
看到 $n$ 大得吓人,于是我们不乱想,考虑贪心。具体来说就是我们发现每次从根结点往下走一定是走到一个叶子的地方,具体来说就是我们可以把树上的一堆边看作一条条链之后贪心的选。
具体来说就是对于一个结点记录以它为起点往下走到叶子能够得到的最优价值,所以我们可以遍历每个结点的时候可以更新其父结点能够得到的最优价值,因为当前父结点已经确定了除去走向当前结点的那条路径的最优价值,我们只需要看一下走向当前结点的是不是更优就可以了,注意要是我们成功更新了,就
........................................................................ 该题解待审........................................................................( 剩余 283 个中英字符)
|
|
[COGS 4161] hope I can sort 题目解析
引入
注意到本题的01序列的排序过程可简化为错位数 k(前 c0 个位置中 1 的个数)的链。
因此可以考虑动态规划。
分析
该问题本质是:
- 每步随机选一对 \(i<j\),仅当 \(a_i=1\) 且 \(a_j=0\) 时交换。
- 交换只可能减少 \(k\)(前段错位 1 与后段错位 0 交换),不可能增加 \(k\)。
- 状态转移概率为:
\(P(k \to k-1) = \frac{k^2}{T}, \quad P(k \to k) = 1 - \frac{k^2}{T}\)
其中T=n*(n+1)/2,即总共有多少组可以交换
- 用 DP 计算 \(m\) 步后 \(k=0\) 的概率即可。
- 复杂度 \(O(m \cdot \min(c_0,c_1))\),可过。
本题中\(c_0,c_1\)代表0的个数和1的个数
#include<bits/stdc++.h>
using namespace std;
const int MOD=998244353,N=5005;
int a[N],dp[N],nd[N];
int qp(int a,int b){
int r=1;
while(b){
if(b&1)r=1ll*r*a%MOD;
a=1ll*a*a%MOD;
b>>=1;
}
return r;
}
int main(){
freopen("hopeicansort.in","r",stdin);
freopen("hopeicansort.out","w",stdout);
int n,m;
cin>>n>>m;
int c0=0;
for(int i=1;i<=n;++i)
{
cin>>a[i];
if(!a[i]) ++c0;
}
int k0=0;
for(int i=1;i<=c0;++i)
if(a[i])
++k0;
int mx=k0<c0?k0:n-c0;
if(mx<k0) mx=k0;
int T=1ll*n*(n-1)/2%MOD,invT=qp(T,MOD-2);
dp[k0]=1;
for(int t=0;t<m;++t)
{
for(int k=0;k<=mx;++k) nd[k]=0;
nd[0]=(dp[0]+1ll*dp[1]*invT)%MOD;
for(int k=1;k<=mx;++k)
{
int d=1ll*k*k%MOD*invT%MOD;
nd[k]=(1ll*dp[k]*(1-d+MOD)+1ll*dp[k+1]*(k+1)%MOD*(k+1)%MOD*invT)%MOD;
}
for(int k=0;k<=mx;++k) dp[k]=nd[k];
}
cout<<dp[0];
return 0;
}
更新日志
2026.2.11 创建题解
|
终焉折枝
积分:1451
提交:197 / 359
|
T4 - Constructive 题解
题目简述
题目大意:
给定 $N$ 种二维向量 $v_i = (a_i, b_i)$ 及其代价 $c_i$,每种向量可无限次使用。求构造出目标向量 $u = (x, y)$ 的最小总代价。
-
向量数量 $N \le 90$
-
目标坐标 $x, y \le 10^9$
-
向量分量 $a_i, b_i \le 10$
-
向量代价 $c_i \le 10^9$
The Key:这是一个 二维无界背包问题,或者等价于 二维网格上的最短路问题。
子任务简析
从简单情况到一般情况的思考过程:
-
Subtask 1: 步长为 1 的坐标移动,结果显而易见。
-
Subtask 2 & 3: 坐标范围小,直接建图跑
Dijkstra。
-
Subtask 4: 降维打击,转化为一维无界背包。
-
Subtask 5: 大坐标直线路径,引入同余类 BFS/DP 思想。
正解
解法 1:分治(官方题解)
几何中点引理:
如果一组短向量的和为 $(x, y)$,那么我们通过调整顺序,一定可以把他们分成两半,使得每一半的和都在 $(\frac{x}{2}, \frac{y}{2})$ 的常数范围内。
转移方程:
$f(x, y) = \min_{\delta_x, \delta_y} \{ f(\lfloor \frac{x}{2} \rfloor + \delta_x, \lfloor \frac{y}{2} \rfloor + \delta_y) + f(\lceil \frac{x}{2} \rceil - \delta_x, \lceil \frac{y}{2} \rceil - \delta_y) \}$
C++ 实现 (官方 std 风格)
#include <bits/stdc++.h>
using namespace std;
const long long INF = 4e18;
const int MAX_LENGTH = 10;
long long one_vec[MAX_LENGTH + 1][MAX_LENGTH + 1];
unordered_map <long long, long long> dp;
const int MAX_X = 1000000001;
long long DP(long long x, long long y){
long long pos = x * MAX_X + y;
if(dp.count(pos)) return dp[pos];
long long tmp = INF;
if(x <= MAX_LENGTH && y <= MAX_LENGTH) tmp = one_vec[x][y];
for (long long dx = -MAX_LENGTH; dx <= MAX_LENGTH; dx++){
for (long long dy = -MAX_LENGTH; dy <= MAX_LENGTH; dy++){
long long x1 = x / 2 + dx, y1 = y / 2 + dy;
long long x2 = x - x1, y2 = y - y1;
if(min({x1, x2, y1, y2}) < 0) continue;
if(x1 + y1 == 0 || x2 + y2 == 0) continue;
tmp = min(tmp, DP(x1, y1) + DP(x2, y2));
}
}
return dp[pos] = tmp;
}
int main(){
int N; long long x, y;
cin >> N >> x >> y;
for (int i = 0; i <= MAX_LENGTH; i++)
for (int j = 0; j <= MAX_LENGTH; j++) one_vec[i][j] = INF;
for (int a, b, c; N--;){
cin >> a >> b >> c;
one_vec[a][b] = min(one_vec[a][b], (long long)c);
}
long long ans = DP(x, y);
if(ans >= INF) cout << "-1\n";
else cout << ans << '\n';
return 0;
}
解法 2:基底 + 小范围微调
核心逻辑:我们先在原点附近进行小范围的精确搜索(微调),然后通过解二元一次方程组,利用“性价比”最高的基底向量组快速跨越远距离。
[Image of Vector Decomposition]
$$(x, y) = \underbrace{(dx, dy)}_{\text{微调部分}} + \underbrace{k_i v_i + k_j v_j}_{\text{基底部分}}$$
C++ 实现 (基底枚举法)
#include<iostream>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const ll INF = 4e18;
struct vec { int a, b; ll c; } v[105];
struct node {
int x, y; ll d;
bool operator > (const node& o) const { return d > o.d; }
};
int n; ll tx, ty, res = INF;
ll dp[205][205];
int main(){
ios::sync_with_stdio(0); cin.tie(0);
cin >> n >> tx >> ty;
for(int i = 1; i <= n; i++) cin >> v[i].a >> v[i].b >> v[i].c;
for(int i = 0; i <= 30; i++)
for(int j = 0; j <= 30; j++) dp[i][j] = INF;
priority_queue<node, vector<node>, greater<node>> pq;
dp[0][0] = 0; pq.push({0, 0, 0});
while(!pq.empty()){
node cur = pq.top(); pq.pop();
if(cur.d > dp[cur.x][cur.y]) continue;
for(int i = 1; i <= n; i++){
int nx = cur.x + v[i].a, ny = cur.y + v[i].b;
if(nx <= 30 && ny <= 30 && dp[nx][ny] > cur.d + v[i].c){
dp[nx][ny] = cur.d + v[i].c;
pq.push({nx, ny, dp[nx][ny]});
}
}
}
for(int i = 1; i <= n; i++){
for(int j = i; j <= n; j++){
ll det = 1LL * v[i].a * v[j].b - 1LL * v[j].a * v[i].b;
for(int dx = 0; dx <= 30; dx++){
for(int dy = 0; dy <= 30; dy++){
if(dp[dx][dy] == INF) continue;
ll rx = tx - dx, ry = ty - dy;
if(rx < 0 || ry < 0) continue;
if(det != 0){
ll na = rx * v[j].b - ry * v[j].a;
ll nb = ry * v[i].a - rx * v[i].b;
ll d = det;
if(d < 0) { d = -d; na = -na; nb = -nb; }
if(na >= 0 && nb >= 0 && na % d == 0 && nb % d == 0)
res = min(res, dp[dx][dy] + (na / d) * v[i].c + (nb / d) * v[j].c);
} else if(1LL * v[i].a * ry == 1LL * v[i].b * rx){
ll k = -1;
if(v[i].a != 0 && rx % v[i].a == 0) k = rx / v[i].a;
else if(v[i].b != 0 && ry % v[i].b == 0) k = ry / v[i].b;
else if(v[i].a == 0 && v[i].b == 0 && rx == 0 && ry == 0) k = 0;
if(k >= 0) res = min(res, dp[dx][dy] + k * v[i].c);
}
}
}
}
}
if(res >= INF) cout << -1 << '\n';
else cout << res << '\n';
return 0;
}
|
终焉折枝
积分:1451
提交:197 / 359
|
T3 - Communication 题解
题目简述
题目大意:
给定三个长度为 $N$ 的序列 $a, b, w$ 和两个参数 $L, R$。构造一张有向图,点 $u \to v$ 有边当且仅当 $L \le a_u + b_v \le R$。求从起点出发,到所有点的最短路(最短路定义为路径点权之和)。
- 数据规模:$N \le 2 \times 10^5$
- 点权限制:$w_i \ge 0$
The Key:这是一个隐式建图的最短路问题。
核心矛盾在于边数可能达到 $O(N^2)$,必须利用边存在的代数条件 $b_v \in [L - a_u, R - a_u]$ 进行区间优化寻点。
子任务分析
- Subtask 1 ($N \le 7$):极小数据,全排列或暴力搜索。
- Subtask 2 ($a_i$ 恒定):连边具有一致性,只需判断 $a_0 + b_v$ 是否合法。
- Subtask 3 ($N \le 10^3$):显式建边跑
Dijkstra。
- Subtask 4 ($L = 1$):转化为前缀连边,按 $b_i$ 排序后建链优化。
- Subtask 5 ($N \le 5 \times 10^4$):典型的线段树优化建图区间连边。
正解:Dijkstra + Set 优化寻点
核心算法流程
基于 Dijkstra 的贪心性质:当一个点从优先队列弹出时,其最短路已确定。我们只需解决:如何快速找到未访问且满足条件的邻居 $v$。
- 将所有尚未确定最短路点(除了起点)存入
std::set,按 $b_i$ 值升序排列。
- 每次从优先队列中取出当前最短路最小的点 $u$。
- 在
set 中执行 lower_bound,定位满足 $b_v \ge L - a_u$ 的最小位置。
- 从该位置开始向后迭代,只要满足 $b_v \le R - a_u$:
- 更新 $v$ 的最短路。
- 将 $v$ 加入优先队列。
- 关键点:从 set 中永久删除 v。
复杂度保证:由于每个点在 set 中仅会被删除一次,迭代器移动的总次数为 $O(N)$。整体复杂度受控于 set 的查询与删除,为 $O(N \log N)$。
C++ 正解实现 (std 风格)
#include <bits/stdc++.h>
#define uwu return 0;
using namespace std;
int main(){
cin.tie(0), ios::sync_with_stdio(0);
int N, L, R;
cin >> N >> L >> R;
vector<int> a(N), b(N);
vector<long long> w(N);
for(auto &i : a) cin >> i;
for(auto &i : b) cin >> i;
for(auto &i : w) cin >> i;
// 按 b 值排序存储待访问点
set <pair<int, int>> sort_by_b;
priority_queue <pair<long long, int>> pq;
vector <long long> dist(N, -1);
for (int i = 1; i < N; i++) {
sort_by_b.insert({b[i], i});
}
pq.push({-w[0], 0}); // w[0] 为起点距离,取负值用于大根堆模拟小根堆
while(!pq.empty()){
pair <long long, int> tp = pq.top();
pq.pop();
// 剪枝:如果已经确定过更短路径,跳过
if (dist[tp.second] != -1 && dist[tp.second] < -tp.first) continue;
dist[tp.second] = -tp.first;
// 在 set 中查找 b[v] 在 [L - a[u], R - a[u]] 范围内的所有点
auto it = sort_by_b.lower_bound({L - a[tp.second], -1});
while (it != sort_by_b.end() && it->first + a[tp.second] <= R) {
pq.push({tp.first - w[it->second], it->second});
// 访问后立即删除,确保每个点只被处理一次
it = sort_by_b.erase(it);
}
}
for(int i = 0; i < N; i++) {
cout << dist[i] << (i == N - 1 ? "" : " ");
}
cout << '\n';
uwu;
}
|
终焉折枝
积分:1451
提交:197 / 359
|
题目简述
题目大意:
给定三个长度为 $N$ 的序列 $a, b, w$ 和两个参数 $L, R$。构造一张有向图,点 $u \to v$ 有边当且仅当:
$L \le a_u + b_v \le R$
求从起点出发,到所有点的最短路(路径上所有点的点权 $w_i$ 之和)。
The Key:
这是一个隐式建图的最短路问题。由于边数可能达到 $O(N^2)$,核心优化在于利用条件 $b_v \in [L - a_u, R - a_u]$ 进行区间连边或范围搜索。
子任务分析
| 子任务 |
特点 / 策略 |
复杂度 |
| Subtask 1 |
$N \le 7$,全排列枚举或 DFS |
$\mathcal{O}(N!)$ |
| Subtask 3 |
$N \le 10^3$,直接显式建边 |
$\mathcal{O}(N^2)$ |
| Subtask 5 |
线段树优化建图 |
$\mathcal{O}(N \log N)$ |
正解思路:Dijkstra + 集合优化
在 Dijkstra 算法执行过程中,由于点权 $w_i \ge 0$,一旦一个点从优先队列中取出,其最短路即确定。我们不需要真的把边连出来。
执行流程:
- 将所有尚未确定最短路的点放入一个按 $b_i$ 排序的
std::set(或平衡树)。
- 从优先队列弹出当前最短路点 $u$。
- 在
set 中利用 lower_bound 寻找满足 $b_v \ge L - a_u$ 的第一个点。
- 向后遍历
set,只要满足 $b_v \le R - a_u$:
- 更新点 $v$ 的最短路。
- 将 $v$ 加入优先队列。
- 关键:将 $v$ 从
set 中永久删除(因为 $v$ 的最短路已找到,不会再被其他点松弛)。
由于每个点仅被删除一次,总的时间复杂度为 $\mathcal{O}(N \log N)$。
C++ 实现代码
#include <bits/stdc++.h>
using namespace std;
int main() {
cin.tie(0), ios::sync_with_stdio(0);
int N, L, R;
cin >> N >> L >> R;
vector<int> a(N), b(N);
vector<long long> w(N);
for(int &i : a) cin >> i;
for(int &i : b) cin >> i;
for(long long &i : w) cin >> i;
set<pair<int, int>> sort_by_b;
priority_queue<pair<long long, int>> pq;
vector<long long> dist(N, -1);
for (int i = 1; i < N; i++) sort_by_b.insert({b[i], i});
pq.push({-w[0], 0});
while(!pq.empty()) {
pair<long long, int> tp = pq.top();
pq.pop();
int u = tp.second;
long long d = -tp.first;
if (dist[u] != -1) continue;
dist[u] = d;
auto it = sort_by_b.lower_bound({L - a[u], -1});
while(it != sort_by_b.end() && it->first + a[u] <= R) {
pq.push({-(d + w[it->second]), it->second});
it = sort_by_b.erase(it); // 每个点只会被删除一次
}
}
for(int i = 0; i < N; i++) cout << dist[i] << (i == N-1 ? "" : " ");
return 0;
}
|
终焉折枝
积分:1451
提交:197 / 359
|
题目简述
题目大意:
给定一棵 $N$ 个节点的有根树,根为 1。每个节点有一个初始颜色 $c_i \in [0, k-1]$。
操作:选择一个节点 $u$ 和颜色 $p$,将 $u$ 及其子树内所有节点的颜色 $c_v \leftarrow (c_v + p) \pmod k$。
目标:通过若干次操作,使所有节点颜色变为 0。题目要求在发生 $Q$ 次单点颜色修改 $(w_i, x_i)$ 后,分别求出达成目标所需的最少操作次数。
-
$N, Q, k \le 2 \times 10^5$
The Key:利用 差分 的思想,将子树修改转化为对“父子颜色关系”的维护。这是一个典型的树上差分或转化贡献维度的思维题。
子任务分析
Subtask 1:树是一条链
当树是一条链(例如 $1 \to 2 \to \dots \to N$)时,我们可以利用序列差分的思想。定义 $d_i = (c_i - c_{i-1}) \pmod k$,其中 $c_0 = 0$。
对于以 $u$ 为根的子树(链上的后缀)进行操作,只会改变 $d_u$ 的值,而不会影响 $d_{u+1} \dots d_N$。为了让所有 $c_i = 0$,等价于让所有差分值 $d_i = 0$。如果某位置 $d_i \neq 0$,我们就必须在 $i$ 处进行一次操作来修正它。因此答案为 $\sum_{i=1}^N [d_i \neq 0]$。
时间复杂度:$\mathcal{O}(N + Q)$
Subtask 2:$k=2$
当只有两种颜色时,问题可以简化为统计 坏边。考虑一条边 $(u, v)$,其中 $u$ 是 $v$ 的父节点。
如果在 $v$ 处不进行任何操作,那么 $v$ 的颜色变化量将完全取决于 $u$ 的变化量。即操作后 $c'_v - c'_u \equiv c_v - c_u \pmod k$。要使最终 $c'_v = c'_u = 0$,必须满足 $c_v - c_u \equiv 0$。如果初始 $c_v \neq c_u$,我们就必须在 $v$ 处进行一次操作。对于 $k = 2$,这等价于统计两端颜色不同的边数(根节点视作父节点颜色为 0)。
Subtask 4:$N, Q \le 5 \times 10^4$
对于一般情况,可以考虑 根号分治:
-
按照节点的度数 $\deg$ 分为大点和小点。
-
修改小点时,直接暴力遍历其所有相邻边,更新答案。
-
修改大点时,更新大点的信息,维护大点与大点之间的关系。
时间复杂度:$\mathcal{O}(Q\sqrt{N})$
正解思路
根据上述性质探索,我们可以得到核心结论:
本题等价于维护树上 两端颜色不同的边数。
$Ans = [c_{\text{root}} \neq 0] + \sum_{(u, v) \in E} [c_u \neq c_v]$
动态维护时,对于点 $u$:
-
父边贡献:当 $c_u \neq c_{fa}$ 时,贡献加 1。
-
子边贡献:使用
cnt[u][col] 维护 $u$ 的子节点中颜色为 col 的数量。子边贡献即为 son_amnt[u] - cnt[u][c_u]。
时间复杂度:$\mathcal{O}(N \log N + Q \log N)$
C++ 实现代码
#include <bits/stdc++.h>
using namespace std;
const int SIZE = 2e5 + 5;
map<int, int> amnt_by_color[SIZE];
vector<int> graph[SIZE];
int color[SIZE], pa[SIZE], son_amnt[SIZE];
void dfs(int nd, int rt) {
pa[nd] = rt;
for(auto i : graph[nd]) {
if(i == rt) continue;
amnt_by_color[nd][color[i]]++;
son_amnt[nd]++;
dfs(i, nd);
}
}
int main() {
cin.tie(0), ios::sync_with_stdio(0);
int N, k, Q;
cin >> N >> k >> Q;
for (int i = 1; i <= N; i++) cin >> color[i];
for (int i = 1, u, v; i < N; i++) { cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
dfs(1, 0);
int different_edge = (color[1] == 0 ? 0 : 1);
for (int i = 1; i <= N; i++) { for(auto j : amnt_by_color[i]) { if(j.first != color[i]) different_edge += j.second; } } for (int w, x; Q--; ) { cin >> w >> x;
// 减去修改前的贡献
different_edge -= (color[pa[w]] != color[w] && w != 1);
different_edge -= (son_amnt[w] - amnt_by_color[w][color[w]]);
if(pa[w]) amnt_by_color[pa[w]][color[w]]--;
color[w] = x;
if(pa[w]) amnt_by_color[pa[w]][color[w]]++;
// 加上修改后的贡献
different_edge += (color[pa[w]] != color[w] && w != 1);
different_edge += (son_amnt[w] - amnt_by_color[w][color[w]]);
// 修正根节点特判
if(w == 1) different_edge = (color[1] != 0) + (son_amnt[1] - amnt_by_color[1][color[1]]);
cout << different_edge << '\n'; } return 0; }
|