2025“钉耙编程”中国大学生算法设计春季联赛(8) Problems
好久没打过杭电了,想着这周周五下午的课已经结课了,就来打一打。开局写前两道题结果这两题数据全是错的,害的我差点怀疑人生。
1001. 拼图游戏
题意:一个\(n\times m\)的矩阵,只包含\([1, k]\)的数。求有多少\((x, y)\)满足\((1, 1)\)到\((x, y)\)这个子矩阵包含\(1\)到\(k\)的数至少一次。
没什么思路。直接用bieset草过去。
用bitset记录\(g[i][j]\)表示第\(i\)行前\(j\)个的元素状态。
那么我们可以枚举\(y\),然后看\(x\)需要到哪里才能满足条件。
点击查看代码
const int N = 1010;
std::bitset<2000> g[N][N];
void solve() {
int n, m, k;
std::cin >> n >> m >> k;
std::vector a(n + 1, std::vector<int>(m + 1));
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
std::cin >> a[i][j];
-- a[i][j];
}
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
g[i][j].reset();
g[i][j] = g[i][j - 1];
g[i][j][a[i][j]] = 1;
}
}
int ans = 0;
std::bitset<2000> s;
for (int j = 1; j <= m; ++ j) {
s.reset();
for (int i = 1; i <= n; ++ i) {
s |= g[i][j];
if (s.count() == k) {
ans += n - i + 1;
break;
}
}
}
std::cout << ans << "\n";
}
1002. 缓存系统
题意:有\(n\times m\)个题,第\((i, j)\)个题的数据如果放到内存需要\(s[i][j]\)的容量,否则需要\(a[i][j]\)的代价。对于第\(i\)行,你必须选择一个前缀(可以非空)放到内存。你的内存只有\(x\)的容量,求最小代价。
把每一行分成\(m + 1\)个物品,第\(i\in [0, m]\)个物品重量为前\(i\)个题的内存和,价值为后面所有题的代价和。那么就变成了一个背包问题。
点击查看代码
void solve() {
int n, m, k;
std::cin >> n >> m >> k;
std::vector s(n, std::vector<int>(m));
std::vector a(n, std::vector<i64>(m));
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < m; ++ j) {
std::cin >> s[i][j] >> a[i][j];
}
}
std::vector<std::vector<std::pair<int, i64>>> goods(n);
for (int i = 0; i < n; ++ i) {
i64 sum = std::accumulate(a[i].begin(), a[i].end(), 0ll);
int pre = 0;
goods[i].emplace_back(0, sum);
for (int j = 0; j < m; ++ j) {
pre += s[i][j];
sum -= a[i][j];
if (pre > k) {
break;
}
goods[i].emplace_back(pre, sum);
}
}
const i64 inf = 1e18;
std::vector<i64> f(k + 1);
for (int i = 0; i < n; ++ i) {
std::vector<i64> g(k + 1, inf);
for (auto & [w, v] : goods[i]) {
for (int j = w; j <= k; ++ j) {
g[j] = std::min(g[j], f[j - w] + v);
}
}
f = g;
}
std::cout << f[k] << "\n";
}
1003. 独到寒山顶
题意:一个矩阵,有些地方不能走,你需要从第一行选一个位置出发,求到最后一行任意一个位置最少经过的格子。
多源\(bfs\)。把第一行的点都入队。然后\(bfs\)即可。答案就是最后一行的最小距离。
点击查看代码
void solve() {
int n, m;
std::cin >> n >> m;
std::vector g(n, std::vector<int>(m));
for (int i = 0; i < n; ++ i) {
int k;
std::cin >> k;
while (k -- ) {
int j;
std::cin >> j;
-- j;
g[i][j] = 1;
}
}
const int inf = 1e9;
std::vector d(n, std::vector<int>(m, inf));
std::queue<std::pair<int, int>> q;
for (int i = 0; i < n; ++ i) {
if (g[i][0] == 0) {
q.emplace(i, 0);
d[i][0] = 1;
}
}
const int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
while (q.size()) {
auto [x, y] = q.front(); q.pop();
for (int i = 0; i < 4; ++ i) {
int nx = x + dx[i], ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m || g[nx][ny]) {
continue;
}
if (d[nx][ny] == inf) {
d[nx][ny] = d[x][y] + 1;
q.emplace(nx, ny);
}
}
}
int ans = inf;
for (int i = 0; i < n; ++ i) {
ans = std::min(ans, d[i][m - 1]);
}
std::cout << ans << "\n";
}
1005. 咒语附魔
题意:给你两个二进制串\(s, t\),从第二个串里选一个和第一个串长度相等的子串,要求异或最大。求这个最大串的1的个数。保证数据随机。
切入点为数据随机。
想要异或最大,我们应该从高位开始匹配,每次尽量和这一位不一样的位置匹配,那么我们可以记录\(t\)中所有可以匹配的位置,如果一个位置和当前匹配是异或为1的可以留下。如果没有一个异或为1的则全部保留。
因为是随机数据,则\(0\)和\(1\)的个数差不多是占\(\frac{1}{2}\),那么每次保留的位置会减半。
点击查看代码
void solve() {
int n, m;
std::cin >> n >> m;
std::string s, t;
std::cin >> s >> t;
std::vector<int> p;
for (int i = 0; i + n - 1 < m; ++ i) {
p.push_back(i);
}
int ans = 0;
for (int i = 0; i < n; ++ i) {
std::vector<int> np;
for (auto & j : p) {
if (s[i] != t[j]) {
np.push_back(j + 1);
}
}
if (np.size()) {
p = np;
++ ans;
} else {
for (auto & j : p) {
++ j;
}
}
}
std::cout << ans << "\n";
}
1007. 架子鼓
题意:给你两组分数,求它们的前缀和有几个相同的。
直接实现一个分数类。然后用\(map\)存第一组的前缀和,接下来枚举第二组前缀和看有没有在第一组出现过。
点击查看代码
struct Node {
i64 a, b;
};
bool operator == (const Node & a, const Node & b) {
return a.a == b.a && a.b == b.b;
}
i64 lcm(i64 n, i64 m) {
return n / std::gcd(n, m) * m;
}
Node operator + (const Node & a, const Node & b) {
if (a.a == 0) {
return b;
}
if (b.a == 0) {
return a;
}
i64 m = lcm(a.b, b.b);
i64 n = a.a * (m / a.b) + b.a * (m / b.b);
i64 d = std::gcd(n, m);
n /= d, m /= d;
return Node{n, m};
}
void solve() {
int n, m;
std::cin >> n >> m;
std::vector<Node> a(n), b(m);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i].a >> a[i].b;
}
for (int i = 0; i < m; ++ i) {
std::cin >> b[i].a >> b[i].b;
}
std::map<std::pair<i64, i64>, int> mp;
Node pre{0, 1};
for (int i = 0; i + 1 < n; ++ i) {
pre = pre + a[i];
++ mp[{pre.a, pre.b}];
}
int ans = 1;
pre = {0, 1};
for (int i = 0; i + 1 < m; ++ i) {
pre = pre + b[i];
ans += mp[{pre.a, pre.b}];
}
std::cout << ans << "\n";
}
1009. 能量网络
题意:\(n\)个点,\(m\)条边,每个点有三个坐标,一条边的三个坐标等于它连接的\((u, v)\)对应三个坐标的和。两个边的距离为\(\min(|x_i - x_j|, |y_i - y_j|, |z_i - z_j|)\)把边当成点求最小生成树。
分别按照三个坐标排序,然后个相邻两个边连边。做\(kruskal\)。
因为一条边一定是选距离它某个坐标最近的一条边。
点击查看代码
struct DSU {
std::vector<int> fa, cnt;
DSU(int _n) {
init(_n);
}
void init(int _n) {
fa.assign(_n, 0);
cnt.assign(_n, 1);
std::iota(fa.begin(), fa.end(), 0);
}
int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
bool merge(int x, int y) {
x = find(x), y = find(y);
if (x == y) {
return false;
}
fa[y] = x;
cnt[x] += cnt[y];
return true;
}
bool same(int x, int y) {
return find(x) == find(y);
}
int size(int x) {
return cnt[find(x)];
}
};
void solve() {
int n, m;
std::cin >> n >> m;
using A = std::array<int, 3>;
std::vector<A> a(n);
for (int i = 0; i < n; ++ i) {
int x, y, z;
std::cin >> x >> y >> z;
a[i] = {x, y, z};
}
std::vector<std::array<int, 4>> b(m);
for (int i = 0; i < m; ++ i) {
int u, v;
std::cin >> u >> v;
-- u, -- v;
b[i] = {a[u][0] + a[v][0], a[u][1] + a[v][1], a[u][2] + a[v][2], i};
}
std::vector<A> edges;
std::sort(b.begin(), b.end());
for (int i = 0; i + 1 < m; ++ i) {
edges.push_back(A{b[i + 1][0] - b[i][0], b[i][3], b[i + 1][3]});
}
std::sort(b.begin(), b.end(), [&](std::array<int, 4> & a, std::array<int, 4> & b) {
return a[1] < b[1];
});
for (int i = 0; i + 1 < m; ++ i) {
edges.push_back(A{b[i + 1][1] - b[i][1], b[i][3], b[i + 1][3]});
}
std::sort(b.begin(), b.end(), [&](std::array<int, 4> & a, std::array<int, 4> & b) {
return a[2] < b[2];
});
for (int i = 0; i + 1 < m; ++ i) {
edges.push_back(A{b[i + 1][2] - b[i][2], b[i][3], b[i + 1][3]});
}
std::sort(edges.begin(), edges.end());
i64 ans = 0;
DSU d(m);
for (auto & [w, u, v] : edges) {
if (d.same(u, v)) {
continue;
}
ans += w;
d.merge(u, v);
}
std::cout << ans << "\n";
}
1010. 字符串哈希
题意:题目给了两个\(hash\)函数:
求有多少长度小于等于\(k\)的字符串满足\(B(s)^3 \times c + B(s)^2 \times d + B(s) \times e + f = A(s)\)。
观察到\(k\)十分小,我们可以通过\(dp\)看长度小于等于\(k\)的串里\([0, 10007)\)的这些值是否能出现。可以通过\(f[i][j]\)推出\(f[i + 1][(j + k \times 10^i) \% 10007], k\in [1, 26]\),类似一个背包。
然后我们可以发现\(A(s)\)其实就是一个\(27\)进制数,于是可以求\(s\in [1, 10007]\)的\(27\)进制表示,它里面不能除了高位连续的1外中间出现一个1。因为\(ord(s_i)\)的值是\([1, 26]\)之间,不可能出现\(0\)。
然后把这些合法的状态推出它的\(B()\)的值,判断是不是和\(s\)相等。
点击查看代码
const int N = 10007;
void solve() {
i64 k, c, d, e, f;
std::cin >> k >> c >> d >> e >> f;
std::vector<i64> p10(k + 1), p27(k + 1);
p10[0] = p27[0] = 1;
for (int i = 1; i <= k; ++ i) {
p10[i] = p10[i - 1] * 10;
p27[i] = p27[i - 1] * 27;
}
std::vector<i64> dp(N);
dp[0] = 1;
for (int i = 0; i < k; ++ i) {
auto ndp = dp;
for (int j = 0; j < N; ++ j) {
if (!dp[j]) {
continue;
}
for (int k = 1; k <= 26; ++ k) {
ndp[(j + k * p10[i] % N) % N] |= dp[j];
}
}
dp = ndp;
}
i64 ans = 0;
for (int i = 1; i < N; ++ i) {
if (!dp[i]) {
continue;
}
i64 s = (i64)i * i * i * c + (i64)i * i * d + (i64)i * e + f;
if (s >= p27[k]) {
continue;
}
int cnt = 0;
std::vector<int> a(k);
for (int j = 0; j < k; ++ j) {
i64 p = s % p27[j + 1] / p27[j];
a[j] = p;
}
bool flag = true;
for (int i = 1; i < k; ++ i) {
if (a[i] != 0 && a[i - 1] == 0) {
flag = false;
break;
}
}
if (flag && a[0] != 0) {
int val = 0;
for (int i = 0; i < k; ++ i) {
val = (val + (i64)a[i] * p10[i] % N) % N;
}
if (val == i) {
++ ans;
}
}
}
std::cout << ans << "\n";
}