2025“钉耙编程”中国大学生算法设计春季联赛(1)
1001. 签到
点击查看代码
void solve() {
int n;
std::string s;
std::cin >> n >> s;
int ans = -1;
for (int i = 1; i <= n; ++ i) {
std::string t;
std::cin >> t;
if (s == t) {
ans = i;
}
}
std::cout << ans << "\n";
}
1002. 船长
参考官方题解补的。
题意:\(n\)个人打选拔赛,按编号从小到大两个两个打,多出来的直接晋级。给出赢家的编号,它和任意一个人打都能赢,其它人赢的概率都是\(\frac{1}{2}\)。有\(k\)个人赢家不想遇到它们。求不遇到他们任何一个的概率。
这题其实就是模拟,赛时看了一会没思路就去看其它题了。
考虑暴力做法,我们把\(n\)个点都加进来,那么就是直接模拟两个人比就行了,每两个相邻的人产生一个赢家\(i'\),记\(f_i\)为现在第\(i\)个人是不想遇到人的概率,那么\(f_i' = \frac{f_{i\times 2} + f_{i\times 2 + 1}}{2}\)。这样模拟\(log_2n\)次就能得出结果。
但\(n\)太大了,貌似不行,但这个暴力模拟提醒我们,如果一开始\(i\)不是给出的\(k\)个人之一,它的\(f_i\)是\(0\)。只有给出的\(k\)个的点能对答案有贡献。那么我们只模拟这\(k\)个点就行了。每次枚举这些点,如果这个点和赢家比,答案就乘上\(1-f_i\)表示这个人不是\(k\)个人之一的概率。否则看第\(i\)个和第\(i+1\)的编号是不是这一轮比,如果是就贡献一个新点\(\frac{f_i + f_{i+1}}{2}\),否则如果这个人不是多出来的就直接乘上\(\frac{1}{2}\),否则就保留\(f_i\)。每次更新这一轮留下的点的编号和概率,模拟\(log_2n\)轮。可以把编号都减一,这样两个比赛的人的编号除二是相同的编号,且两个人的编号异或1就是另一个人的编号。便于维护。
代码省略取模类
点击查看代码
void solve() {
int n, k, win;
std::cin >> n >> k >> win;
-- win;
std::vector<int> a(k);
std::vector<Z> f(k, 1);
for (int i = 0; i < k; ++ i) {
std::cin >> a[i];
-- a[i];
}
std::sort(a.begin(), a.end());
Z inv2 = (Z)1 / 2;
Z ans = 1;
while (a.size()) {
std::vector<int> b;
std::vector<Z> g;
win >>= 1;
for (int i = 0; i < a.size();) {
if (win == (a[i] >> 1)) {
ans = ans * (1 - f[i]);
i += 1;
} else {
b.push_back(a[i] >> 1);
if (i + 1 < a.size() && (a[i] >> 1) == (a[i + 1] >> 1)) {
g.push_back((f[i] + f[i + 1]) * inv2);
i += 2;
} else if ((a[i] ^ 1) < n) {
g.push_back(f[i] * inv2);
i += 1;
} else {
g.push_back(f[i]);
i += 1;
}
}
}
a = b;
f = g;
n = (n + 1) >> 1;
}
std::cout << ans << "\n";
}
1004. 海浪
题意:给你\(n\)个数,如果一段区间\([l, r]\)存在一个\(h_B\)使得\(i \in [l + 1, r]\)都有\(h_b \in (\min(a_i, a_{i-1}), \max(a_i, a_{i-1}))\)则称\([l, r]\)是波浪。每次求\([l, r]\)内最长的波浪。
记\(lo_i = \min(a_i, a_{i-1}), hi_i = \max(a_i, a_{i-1})\),那么一个区间是波浪,有\(\max_{i=l+1}^{r} lo_i < \min_{i=l+1}^{r} hi_i\)。我们可以用\(st\)表或者线段树维护区间的最大\(lo\)和最小\(hi\),然后二分求出来每个\(R_i\)表示\([i, R_i]\)是波浪,且对\(i\)来说\(R_i\)是最右的下标,同样记录一个\(L_i\),表示\(i\)往左边是波浪最左点。
那么我们考虑离线查询,用一棵线段树维护每个\(i\)往右走的最远距离。发现\(R_i \leq R_{i+1}\),可以固定右端点\(i\),初始\(j=0\),如果\(R_j \leq i\)就把\(R_j - j + 1\)更新\(j\)。那么对于每个查询\([l, i]\),则查询\([l, i]\)的区间最大值就行。然后记得用\(L_l\)和\(R_i\)对应的长度取最大值。
代码省略了取模类。
点击查看代码
template <class Info>
struct ST {
std::vector<std::vector<Info>> st;
ST(std::vector<Info> a) {
int n = a.size(), m = std::__lg(n) + 1;
st.assign(n, std::vector<Info>(m));
for (int i = 0; i < n; ++ i) {
st[i][0] = a[i];
}
for (int j = 1; j < m; ++ j) {
for (int i = 0; i + (1 << j - 1) < n; ++ i) {
st[i][j] = st[i][j - 1] + st[i + (1 << j - 1)][j - 1];
}
}
}
Info query(int l, int r) {
if (l < 0 || r >= st.size() || l > r) {
return {-1, 1};
}
int lg = std::__lg(r - l + 1);
return st[l][lg] + st[r - (1 << lg) + 1][lg];
}
};
struct STInfo {
int lo, hi;
};
STInfo operator + (const STInfo & a, const STInfo & b) {
STInfo res{};
res.lo = std::max(a.lo, b.lo);
res.hi = std::min(a.hi, b.hi);
return res;
}
#define ls (u << 1)
#define rs (u << 1 | 1)
#define umid (tr[u].l + tr[u].r >> 1)
template <class Info>
struct Node {
int l, r;
Info info;
};
template <class Info>
struct SegmentTree {
std::vector<Node<Info> > tr;
SegmentTree(int _n) {
init(_n);
}
SegmentTree(std::vector<Info> & a) {
init(a);
}
void init(int _n) {
tr.assign(_n << 2, {});
build(0, _n - 1);
}
void init(std::vector<Info> & a) {
int _n = (int)a.size();
tr.assign(_n << 2, {});
build(0, _n - 1, a);
}
void pushup(int u) {
tr[u].info = tr[ls].info + tr[rs].info;
}
void build(int l, int r, int u = 1) {
tr[u] = {l, r, {}};
if (l == r) {
return;
}
int mid = l + r >> 1;
build(l, mid, ls); build(mid + 1, r, rs);
}
void build(int l, int r, std::vector<Info> & a, int u = 1) {
tr[u] = {l, r, {}};
if (l == r) {
tr[u].info = a[l];
return;
}
int mid = l + r >> 1;
build(l, mid, a, ls); build(mid + 1, r, a, rs);
pushup(u);
}
void modify(int p, Info add, bool set = false) {
int u = 1;
while (tr[u].l != tr[u].r) {
int mid = umid;
if (p <= mid) {
u = ls;
} else {
u = rs;
}
}
if (set) {
tr[u].info = add;
} else {
tr[u].info = tr[u].info + add;
}
u >>= 1;
while (u) {
pushup(u);
u >>= 1;
}
}
Info query(int l, int r, int u = 1) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].info;
}
int mid = umid;
if (r <= mid) {
return query(l, r, ls);
} else if (l > mid) {
return query(l, r, rs);
}
return query(l, r, ls) + query(l, r, rs);
}
};
struct Info {
int max;
};
Info operator + (const Info & a, const Info & b) {
Info res{};
res.max = std::max(a.max, b.max);
return res;
}
void solve() {
int n, q;
std::cin >> n >> q;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
std::vector<STInfo> info(n);
for (int i = 1; i < n; ++ i) {
int lo = std::min(a[i], a[i - 1]);
int hi = std::max(a[i], a[i - 1]);
info[i] = {lo, hi};
}
ST<STInfo> st(info);
std::vector<int> L(n), R(n);
for (int i = 0; i < n; ++ i) {
int l = 0, r = i;
while (l < r) {
int mid = l + r >> 1;
auto info = st.query(mid + 1, i);
if (info.lo < info.hi) {
r = mid;
} else {
l = mid + 1;
}
}
L[i] = l;
}
for (int i = 0; i < n; ++ i) {
int l = i, r = n - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
auto info = st.query(i + 1, mid);
if (info.lo < info.hi) {
l = mid;
} else {
r = mid - 1;
}
}
R[i] = l;
}
std::vector<std::vector<std::pair<int, int>>> Q(n);
for (int i = 0; i < q; ++ i) {
int l, r;
std::cin >> l >> r;
-- l, -- r;
Q[r].push_back({l, i});
}
std::vector<int> res(q);
SegmentTree<Info> tr(n);
for (int i = 0, j = 0; i < n; ++ i) {
while (R[j] <= i) {
tr.modify(j, Info{R[j] - j + 1});
++ j;
}
for (auto & [l, id] : Q[i]) {
res[id] = tr.query(l, i).max;
res[id] = std::max(res[id], std::min(R[l], i) - l + 1);
res[id] = std::max(res[id], i - std::max(L[i], l) + 1);
}
}
Z ans = 0;
for (int i = 0; i < q; ++ i) {
ans += Z(i + 1) * res[i];
}
std::cout << ans << "\n";
}
1005. 航线
题意:一个\(n \times m\)的矩阵,每个点有一个通过时间\(a[i][j]\),和一个转向时间\(b[i][j]\)。初始你在\((1, 1)\),方向为右,你要到\((n, m)\)往下出去。通过\((i, j)\)需要花费\(a[i][j]\)的代价,每次转向花费\(b[i][j]\)的代价。如果不转向只能沿着当前方向走。求最小代价。
记\(dist[i][j][t]\)表示已经通过了\((i, j)\)方向为\(t\)的最小代价。那么跑\(dijkstra\)即可。
点击查看代码
void solve() {
int n, m;
std::cin >> n >> m;
std::vector a(n, std::vector<i64>(m));
std::vector b(n, std::vector<i64>(m));
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < m; ++ j) {
std::cin >> a[i][j];
}
}
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < m; ++ j) {
std::cin >> b[i][j];
}
}
const int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
const i64 inf = 1e18;
std::vector dist(n, std::vector(m, std::array<i64, 4>{inf, inf, inf, inf}));
using A = std::array<i64, 4>;
std::priority_queue<A, std::vector<A>, std::greater<A>> heap;
dist[0][0][1] = a[0][0];
heap.push({dist[0][0][1], 0, 0, 1});
while (heap.size()) {
auto [d, x, y, t] = heap.top(); heap.pop();
if (d != dist[x][y][t]) {
continue;
}
int nx = x + dx[t], ny = y + dy[t];
if (nx >= 0 && nx < n && ny >= 0 && ny < m) {
if (dist[nx][ny][t] > dist[x][y][t] + a[nx][ny]) {
dist[nx][ny][t] = dist[x][y][t] + a[nx][ny];
heap.push({dist[nx][ny][t], nx, ny, t});
}
}
for (int i = 0; i < 4; ++ i) {
if (dist[x][y][i] > dist[x][y][t] + b[x][y]) {
dist[x][y][i] = dist[x][y][t] + b[x][y];
heap.push({dist[x][y][i], x, y, i});
}
}
}
i64 ans = dist[n - 1][m - 1][2];
std::cout << ans << "\n";
}
1006. 密码
题意:有\(n\)个方程:\(a_i \times x + b_i = c_i\),给出\(u_i, v_i, w_i\)表示每个\((a_i, b_i, c_i)\)取值是它们的某种排列。你要找到一个\(x\)满足所有方程。
对于每个方程对\(u_i, v_i, w_i\)进行全排列枚举\(a_i, b_i, c_i\)的值,如果\((c_i - b_i) \% a_i == 0\)且是非负整数就记录下来。然后枚举第一个方程的所有可能值,看有没有一个在所有方程的可能值里都出现过。
注意用\(next\_permutation\)要先排序。(因为这个wa了6次)。
点击查看代码
void solve() {
int n;
std::cin >> n;
std::vector<std::set<i64>> s(n);
for (int i = 0; i < n; ++ i) {
i64 u, v, w;
std::cin >> u >> v >> w;
std::vector<i64> b{u, v, w};
std::sort(b.begin(), b.end());
do {
if ((b[0] - b[1]) % b[2] == 0 && (b[0] - b[1]) / b[2] >= 0) {
s[i].insert((b[0] - b[1]) / b[2]);
}
} while (std::next_permutation(b.begin(), b.end()));
}
for (auto & x : s[0]) {
bool flag = true;
for (int i = 1; i < n; ++ i) {
if (!s[i].count(x)) {
flag = false;
break;
}
}
if (flag) {
std::cout << x << "\n";
return;
}
}
}
1007. 分配宝藏
题意:你给\(n\)个人分钱,如果有一半的人不满意你就要寄,然后按顺序让位,直到有一个人让一半的人满意。你要怎么分配才能让自己不寄且给出的钱最少。
除了自己,假设只有一个人,那么直接不给。如果有两个人,第一个人肯定想搞你,这样他就可以分钱了,但如果你寄了,第二个人一分钱也没有,所以如果你给第二个人一块钱,他就会支持你。同理思考三个人的情况,四个人的情况。发现只会给第偶数个人一块。于是大胆猜测,就是一个等差数列求和。
代码省略取模类
点击查看代码
void solve() {
i64 n;
std::cin >> n;
n = n / 2 * 2;
Z ans = (Z)n / 2 * (2 + n) / 2;
std::cout << ans << "\n";
}
1009. 切割木材
题意:\(n\)个数分成若干组。每一组的价值是\(g(f(l, r))\)。其中\(g\)是给出的,\(f(l, r)\)是区间按位或减区间按位与的值。
一个显然的\(dp\)是\(f_i\)表示前\(i\)的最大价值,枚举上一组划分到哪里就可以轻松转移。但这样是\(O(n^2)\)的,无法接受。
需要观察的到是,如果固定\(i\),那么\(i \in [1, i]\)的\(f(1, i)\)的不同值最多只有\(m\)个,因为我们从\(i\)开始向左走,那么对于某一位来说,如果\(a_i\)这一位是\(0\),要么使得按位与的值变成\(0\),或者按位或的值变成\(1\),否则两个都不变,发现固定\(i\)的时候,按位或的值和按位与的值每一位就只有一次变化的可能了。(如果\(a_i\)这一位是\(1\),那么这一位的按位或不会变了,如果是\(0\)则按位与不会变了)所以最多变\(m\)次。也就是说,只有\(m\)中不同的按位或和按位与的组合。
于是我们用\(map\)维护每一对按位或和按位与对应的\(f\)的最大值。
点击查看代码
void solve() {
int n, m;
std::cin >> n >> m;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; ++ i) {
std::cin >> a[i];
}
std::vector<int> g(1 << m);
for (int i = 0; i < 1 << m; ++ i) {
std::cin >> g[i];
}
const i64 inf = 1e18;
std::vector<i64> f(n + 1, -inf);
std::map<int, std::map<int, i64>> b;
f[0] = 0;
b[(1 << m) - 1][0] = 0;
for (int i = 1; i <= n; ++ i) {
std::map<int, std::map<int, i64>> c;
for (auto & [sand, it] : b) {
for (auto & [sxor, val] : it) {
if (!c.count(sand & a[i]) || !c[sand & a[i]].count(sxor | a[i])) {
c[sand & a[i]][sxor | a[i]] = val;
} else {
c[sand & a[i]][sxor | a[i]] = std::max(c[sand & a[i]][sxor | a[i]], val);
}
}
}
for (auto & [sand, it] : c) {
for (auto & [sxor, val] : it) {
f[i] = std::max(f[i], val + g[sxor - sand]);
}
}
b = c;
if (!b.count((1 << m) - 1) || !b[(1 << m) - 1].count(0)) {
b[(1 << m) - 1][0] = f[i];
} else {
b[(1 << m) - 1][0] = std::max(b[(1 << m) - 1][0], f[i]);
}
}
std::cout << f[n] << "\n";
}