mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2133 字
5 分钟
2026“钉耙编程”中国大学生算法设计春季联赛(2)
2026-03-27

这个网站是 雨沐ネブラ (Amekai_nebula) 的个人博客网站

未来这个网站会更新若干内容,包括不局限于题解、感悟等内容

1001-湖上午后#

题意: 给出两个非负整数 sstt, 求出方程 a×b=(a or b)×(a and b)a \times b = (a \text{ or } b) \times (a \text{ and } b)a,ba, b 都取 [s,t][s, t] 中整数时一共有多少组解。答案对 998244353 取模。

有一个经典的等式是 a+b=(a&b)+(ab)a + b = (a \& b) + (a | b), 知道了这个之后可以记 x=a&b,y=abx = a \& b, y = a | b

这样两个式子组合就是 a×b=x×y,a+b=x+ya \times b = x \times y, a + b = x + y 。最后只有 a=x,b=ya = x, b = y 或者 a=y,b=xa = y, b = x 两种解。但是只需要求一种就行,另外一部分可以直接乘2得到

假设这里是 a=xa = xaa 的二进制为1的位就必须是 bb 的子集,这里就要用到数位dp了,定义 dp[i][j][k][r] (i : 当前是从高到低第几位,j : 是否紧贴最小值,k : 是否紧贴最大值,r : 是否有 a < b) 这个dp第一维度可以压掉,所以后面代码中下标 ii 代表这里的 jj,剩下的一次对应。初始值也就是紧贴上下界,没有 a<ba < b 关系 dp[1][1][0] = 1

之前有提到子集的关系,所以 ai,bia_i, b_i 的情况只有三种 {0, 0}, {0, 1}, {1, 1}。这里转移只说一下 jj 的变化,其他也是类似的。如果 j=truej = true 说明此时挨着下界,所以 aia_i 的这一位 不能小于 sis_i if(i == 1 && ai < si) continue; 如果当前依然有 ai=sia_i = s_i 那么 jj 就还是 1 ,否则就变化为0 int ii = (i == 1 && ai == si) ? 1 : 0;

你可能会好奇为什么会有一个 kk 用来表示 a<ba < b 。但这其实是题目要求是这个,我们已有的限制只能保证 aba \le b

详细转移看下面代码,数位dp一般都从最高位开始,方便确定大小关系。数位dp还是太难写了!

void solve(){
string s, t;
cin >> s >> t;
int tt = 0, ss = 0;
for(char c : s) {
ss = (ss * 2 + c - '0') % mod;
}
for(char c : t) {
tt = (tt * 2 + c - '0') % mod;
}
int ans = (tt - ss + 1 + mod) % mod;
if (s.size() < t.size()) {
s = string(t.size() - s.size(), '0') + s;
} else if (t.size() < s.size()) {
t = string(s.size() - t.size(), '0') + t;
}
int n = s.length();
int dp[2][2][2] = {0};
vector<pii> ch = {{0, 0}, {0, 1}, {1, 1}};
dp[1][1][0] = 1;
fore(_, 0, n) {
int si = s[_] - '0', ti = t[_] - '0';
int ndp[2][2][2] = {0};
fore(i, 0, 2) fore(j, 0, 2) fore(k, 0, 2) {
for(auto[ai, bi] : ch) {
if(i == 1 && ai < si) continue;
int ii = (i == 1 && ai == si) ? 1 : 0;
if(j == 1 && bi > ti) continue;
int jj = (j == 1 && bi == ti) ? 1 : 0;
int kk = (k == 0 && ai == bi) ? 0 : 1;
ndp[ii][jj][kk] += dp[i][j][k];
ndp[ii][jj][kk] %= mod;
}
}
memcpy(dp, ndp, sizeof(dp));
}
int S = 0;
fore(i, 0, 2) fore(j, 0, 2) {
S = (S + dp[i][j][1]) % mod;
}
S = S * 2 % mod;
ans = (ans + S) % mod;
cout << ans << endl;
}

1002-庭扫落樱#

题意: 在坐标系的第一象限有 nn 个激光发射器,每个位于 xi,yix_i, y_i 每个都有一个方向 d{0,1,2,3}d \in \{0, 1, 2, 3\} 分表代表上右下左。输出最少放置多少个障碍物可以阻挡所有射线,保证不会有两个发射器互相照射,障碍物可以放在发射器那个点上

容易发现有些发射器不用单独去管,如果两个发射器方向相同并且一个发射器光线在另一个的光线上,后面那个发射器就可以排除开

这样排除一些之后最少障碍物数量 = 总数 - 重合的点数

尽量省就要一个障碍物覆盖多个光线,这里删掉一些后最多就是覆盖两个,这就有点像二分图了,上下和左右作为二分图左侧和右侧,两个光线有交点就把他们连线。结果就是总数减去最大匹配数

class BP {
public:
vector<int> vis, link;
vector<vector<int>> p;
int n, m;
BP(int n, int m) : n(n), m(m) {
p.resize(n + 1);
vis.resize(m + 1, false), link.resize(m + 1, -1);
}
inline void add(int u, int v) { p[u].emplace_back(v); };
bool dfs(int s) {
for(int to : p[s]) {
if(vis[to]) continue;
vis[to] = true;
if(link[to] == -1 || dfs(link[to])) {
link[to] = s;
return true;
}
}
return false;
}
int match() {
int ans = 0;
fore(i, 1, n + 1) {
fill(vis.begin() + 1, vis.end(), false);
if(dfs(i)) ans++;
}
return ans;
}
};
void solve(){
int n;
cin >> n;
map<int, int> mp1, mp2, mp3, mp4;
vector<pii> up, down, right, left;
int x, y, d;
fore(i, 1, n + 1) {
cin >> x >> y >> d;
if(d == 0) {
if(!mp1.count(x)) mp1[x] = y;
else mp1[x] = max(mp1[x], y);
}else if(d == 1) {
if(!mp2.count(y)) mp2[y] = x;
else mp2[y] = max(mp2[y], x);
}else if(d == 2) {
if(!mp3.count(x)) mp3[x] = y;
else mp3[x] = min(mp3[x], y);
}else {
if(!mp4.count(y)) mp4[y] = x;
else mp4[y] = min(mp4[y], x);
}
}
for(auto[x, y] : mp1) up.emplace_back(x, y);
for(auto[x, y] : mp3) down.emplace_back(x, y);
for(auto[y, x] : mp2) right.emplace_back(x, y);
for(auto[y, x] : mp4) left.emplace_back(x, y);
BP bp(up.size() + down.size(), right.size() + left.size());
int l = 0;
for(int i = 0; i < up.size(); i++) {
l++;
x = up[i].first, y = up[i].second;
int r = 0;
for(auto[xx, yy] : right) {
r++;
if(xx <= x && yy >= y) bp.add(l, r);
}
for(auto[xx, yy] : left) {
r++;
if(xx >= x && yy >= y) bp.add(l, r);
}
}
for(int i = 0; i < down.size(); i++) {
l++;
x = down[i].first, y = down[i].second;
int r = 0;
for(auto[xx, yy] : right) {
r++;
if(xx <= x && yy <= y) bp.add(l, r);
}
for(auto[xx, yy] : left) {
r++;
if(xx >= x && yy <= y) bp.add(l, r);
}
}
cout << up.size() + down.size() + right.size() + left.size() - bp.match() << endl;
}

1003-竹林清韵#

题意: 给出一个有 nn 个数的数组,铃仙每次可以选取任意一个数,因幡帝会固定选择数组的前 mm 个数,对所有的 m=1,2,3,4...n1m = 1, 2, 3, 4 ... n - 1 最大化铃仙所选数的和

题目条件翻译一下就是数组的前 k×(m+1)k \times (m + 1) 个数中最多只能选择 kk 个数。如果 mm 是固定的,那这个直接用小根堆即可,每次碰到新的数都尝试加入堆中,如果容量超出可选数的限制就 pop 掉一个,最后统计和即可

但这里是所有的 mm 这么做就是 n2log(n)n^2log(n) 肯定不行了,关键是从前面开始这个至多选几个很处理,从后开始往往会得到比较好的性质: 假设最后总共要选 kk 个数, 从 (ki)×(m+1)+1(k - i) \times (m + 1) + 1nn 至少选 ii 个数,这个至少选多少个就容易处理的多,因为这里已经选择出来的数之后不会被替代了,之前已经选的是可能被后面更大的数更换的

每次只需要知道某个区间最大值即可,这个可以用线段树维护,当某个数被选择时就将其替换为最小值就等同删除了。总共选择的数的个数是一个调和级数,每次查询修改是 log(n)log(n) 最终复杂度是 nlog2nnlog^2n

class SegTree{
private:
int n;
vector<int> a;
vector<pii> info;
pii bet(pii x, pii y) {
if(x.first != y.first) return x.first > y.first ? x : y;
return x.second < y.second ? x : y;
}
void build(int i, int l, int r) {
if(l == r)
info[i] = {a[l], l};
else {
int mid = (l + r) >> 1;
build(i << 1, l, mid);
build(i << 1 | 1, mid + 1, r);
info[i] = bet(info[i << 1], info[i << 1 | 1]);
}
}
void set(int pos, int val, int l, int r, int i) {
if(pos < 1 || pos > n) return;
if(l == r) {
a[pos] = val;
info[i] = {val, pos};
return;
}
int mid = (l + r) >> 1;
if(pos <= mid) set(pos, val, l, mid, i << 1);
else set(pos, val, mid + 1, r, i << 1 | 1);
info[i] = bet(info[i << 1], info[i << 1 | 1]);
}
pii query(int jobl, int jobr, int l, int r, int i) {
if(jobl <= l && r <= jobr) return info[i];
int mid = (l + r) >> 1;
pii ans = {-1, -1};
if(jobl <= mid) ans = bet(query(jobl, jobr, l, mid, i << 1), ans);
if(jobr > mid) ans = bet(query(jobl, jobr, mid + 1, r, i << 1 | 1), ans);
return ans;
}
public:
SegTree(int n, vector<int>& arr) : n(n), info((n << 2) + 1), a(n + 1) {
fore(i, 1, n + 1) a[i] = arr[i];
build(1, 1, n);
}
void set(int pos, int val) { set(pos, val, 1, n, 1);}
pii query(int jobl, int jobr) {return query(jobl, jobr, 1, n, 1);}
int get(int pos) {return a[pos];}
};
void solve(){
int n;
cin >> n;
vector<int> val(n + 1);
fore(i, 1, n + 1) cin >> val[i];
SegTree seg(n, val);
for(int len = 1; len <= n - 1; len++) {
int ans = 0;
vector<int> used;
for(int k = n / (len + 1); k >= 0; k--) {
if(k * (len + 1) + 1 > n) continue;
auto[maxn, pos] = seg.query(k * (len + 1) + 1, n);
ans += maxn;
used.emplace_back(pos);
seg.set(pos, -inf);
}
for(int c : used) seg.set(c, val[c]);
cout << ans << ' ';
}
cout << endl;
}

1004-三途川畔#

题意: 给定两个非负整数 nnkk,请你构造 nn 个正整数,使得这些数的乘积大于或等于它们的和,并且两者的差恰好等于 kk

合乘积有关那就是1越多越好,所以假设有 n2n - 2 个 1,剩下一下 x,yx, y 。所以目前要满足的关系就是 x×y=x+y+n2+kx \times y = x + y + n - 2 + k

一定要化简!化简得到 (x1)(y1)=n+k+1(x - 1)(y - 1) = n + k + 1 显然 x=2x = 2yy 一定有解 n+kn + k 构造完毕

void solve(){
int n, k;
cin >> n >> k;
fore(i, 1, n - 2 + 1) cout << 1 << ' ';
cout << 2 << ' ' << n + k << endl;
}

1007-云端航迹#

题意: 给出一个有 nn 个数的数组,Alice和Bob轮流在任意一个子数组中玩游戏,Alice先手,每次可以选择任意一个数删除,任何时候集合中所有数的异或和为0,则最后一次操作数组的人输掉游戏,问在所有的子数组中两人分别获胜的次数

Alice 先手想要赢子数组大小必须是偶数,(如果是奇数,对手面对的就是偶数,此时一直总的异或和是 S 且不为 0 ,对方只需要在这个数组中选一个数满足 ai!=Sa_i != S 即可一直拖下去,直到最后Alice选完最后一个数。并且这个事对手一定做得到,假设所有位置的值都是 S ,又因为元素个数是偶数,出现这种Alice已经输了,所以对手一定找得到这个数)。

其次开始这个异或和不是0,这里同样和上面一样思考,我要找一个 ai!=Sa_i != S ,但是是一定找得到的,可以一直拖到对手走到必败态或者最后全部取完

具体实现就是维护前缀异或和,对左边按奇偶分类,并且用cnt数组记录所有前缀中为某个值的位置有多少个

void solve(){
int n;
cin >> n;
vector<int> val(n + 1);
vector<int> pre(n + 1);
fore(i, 1, n + 1) {
cin >> val[i];
pre[i] = pre[i - 1] ^ val[i];
}
vector<int> cnt1(4e5 + 5), cnt2(4e5 + 5);
int even = 0, odd = 0;
int ans = 0;
fore(i, 0, n + 1) {
if(i & 1) {
ans += odd;
odd += 1;
ans -= cnt1[pre[i]];
cnt1[pre[i]]++;
}else {
ans += even;
even += 1;
ans -= cnt2[pre[i]];
cnt2[pre[i]]++;
}
}
cout << ans << ' ' << (n + 1) * n / 2 - ans << endl;
}

1009-微缩庭园#

题意: 给出一个 n×mn \times m 的网格,你可以随意填充黑色,但是必须保证任意两个黑色单元格不共享一条边,并且这个最终的图是对称的,问最多填充多少个黑色方块

网格图有个结论是最多选出 nm/2\lceil nm / 2 \rceil 个不相邻的单元格

如果 n,mn, m 中有一个是奇数,那么交替填充就可以到达这个上界,问题是都是偶数怎么办,这时候就必须舍弃掉一行或者一列,结果就是 nm/2min(n,m)nm / 2 - min(n, m)

void solve(){
int n, m;
cin >> n >> m;
if(n % 2 || m % 2) {
cout << (n * m + 1) / 2 << endl;
}else {
cout << n * m / 2 - min(n, m) << endl;
}
}

1010-静海拾光#

题意: 每个测试用例的共一行,两个数 xxkk ,表示查询所有元素和不超过 xx 的非空正整数序列按字典序升序排序后的第 kk 个序列。保证一定存在

这种问题我们首先要知道的是 f(x)f(x) ,总和是 xx 时的序列一共有多少个,官方题解用的是类似数学归纳法证明的,优点是证明过程简单,但是你得观察出规律,这里有一个更好的证明方式

采用递推思想假设 f(x1)f(x - 1) 是已知的来求 f(x)f(x) ,f(x) 中以1开头的序列可以由 f(x1)f(x - 1) 的所有序列最前面加1得到,不以1开头的我们把每一个序列的第一个元素数值减一,发现得到的就是所有的 f(x1)f(x - 1) 中的序列,所以 f(x)=2f(x1)f(x) = 2f(x - 1) 可以发现虽然 xx 可能很大,但数列个数是指数增长的,超过的部分就不用统计了

由等比数列求和可以得到总和小于等于 xx 的数组数量是 2x12^x - 1g(x)g(x) 中以1开头的序列个数是 g(x1)+1=2x1g(x - 1) + 1 = 2^{x - 1} 这个递推和之前是同一个思想,所以 k2x1k \leq 2^{x - 1} 时就可以确定这一位就是1了,接着找序列的剩余部分元素之和不超过 x1x − 1 ,字典序排名为 k1k − 1 的序列。如果大于1就把所有的序列最高位都减一,这样相对大小是不变的,接着找序列剩余部分元素之和不超过 x1x − 1 ,字典序排名为 k2x1k − 2^{x - 1} 的序列。这和原问题是类似的,一次递推即可,复杂度 O(x)O(x)

void solve(){
int x, k, n = 1;
cin >> x >> k;
vector<int> ans(x + 5);
auto dfs = [&](auto&& dfs, int x, int k) -> void {
// cerr << x << ' ' << k << endl;
ans[n]++;
if(k == 1) {
n++; return;
}
if(x > 60 || k <= p[x - 1]) {
n++;
dfs(dfs, x - 1, k - 1);
}else {
dfs(dfs, x - 1, k - p[x - 1]);
}
};
dfs(dfs, x, k);
fore(i, 1, n) cout << ans[i] << " \n"[i == n - 1];
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

部分信息可能已经过时

目录