2024“钉耙编程”中国大学生算法设计超级联赛(1)
2024“钉耙编程”中国大学生算法设计超级联赛(1)
循环位移
思路
字符串哈希,将 A 串拼接两遍记为 AA,然后对其哈希一下,用 map/set 记录哈希值,因为 \(|A|\le|B|\),所以只要检查 B 中长度为 \(|A|\) 的子串哈希值是否存在 AA 中即可。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
struct Hash {
using u64 = unsigned long long;
u64 base = 13331;
vector<u64> pow, hash;
Hash(string &s) {
s = " " + s;
int N = s.size();
pow.resize(N + 1), hash.resize(N + 1);
pow[0] = 1, hash[0] = 0;
for (int i = 1; i < s.size(); i ++) {
pow[i] = pow[i - 1] * base;
hash[i] = hash[i - 1] * base + s[i];
}
}
u64 get(int l, int r) {
return hash[r] - hash[l - 1] * pow[r - l + 1];
}
//拼接两个子串
u64 link(int l1, int r1, int l2, int r2) {
return get(l1, r1) * pow[r2 - l2 + 1] + get(l2, r2);
}
bool same(int l1, int r1, int l2, int r2) {
return get(l1, r1) == get(l2, r2);
}
};
void solve() {
string a, b;
cin >> a >> b;
int k = a.size();
a = a + a;
Hash HashA(a), HashB(b);
set<i64> s;
for (int i = 0; i + k - 1 < a.size(); i ++) {
s.insert(HashA.get(i, i + k - 1));
}
i64 ans = 0;
for (int i = 0; i + k - 1 < b.size(); i ++) {
if (s.count(HashB.get(i, i + k - 1)))
ans ++;
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
星星
思路
考虑分组背包。
设 \(dp_i\) 表示体积为 i 的星星所需的最小代价。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n, k;
cin >> n >> k;
vector<array<i64, 5>> A(n + 1);
for (int i = 1; i <= n; i ++)
cin >> A[i][1] >> A[i][2] >> A[i][3] >> A[i][4];
vector<i64> dp(k + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= n; i ++) {
for (int v = k; v >= 0; v --) {
for (int j = min(v, 4); j >= 0; j --) {
dp[v] = min(dp[v], dp[v - j] + A[i][j]);
}
}
}
cout << dp[k] << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
树
思路
将 \(\max(a_u,a_v)\times |a_u-a_v|\) 拆开后就是 \(\max^2-\max \times\min\)。
发现计算该结果仅需考虑两个值之间的大小关系,无需关心树的形态,将子树的权值看成一个多重集 \(S\),那么每次往里新增一个 \(a_u\) 假设为 \(x\),那么就是求新增的 \(\sum\limits_{y\in S}f(x,y)\) 的贡献,将上述结论代入分类讨论可得:
- \(x=y\) 时,\(f(x,y)=0\)。
- \(x<y\) 时,\(f(x,y)=y^2-xy\),那么所有小于 \(x\) 的 \(y\) 产生的贡献即为 \(\sum\limits_{y\in S}y^2-x\sum\limits_{y\in S}y\)。
- \(x>y\) 时,\(f(x,y)=x^2-xy\),那么所有大于 \(x\) 的 \(y\) 产生的贡献即为 \(x^2\sum\limits_{y\in S}1-x\sum\limits_{y\in S}y\)。
上述中的 \(\sum1,\sum y,\sum y^2\) ,可用权值树状数组进行维护,又因为每个节点答案由其子树和其本身得到,支持离线,所有可以直接套上 dsu on tree 处理。
最后 \(\times 2\) 是因为 \(f(a_u,a_v)\) 和 \(f(a_v,a_u)\) 需要计算 2 次。
模数为 \(2^{64}\) 可以使用 unsigned long long 类型自然溢出即可。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
using u64 = unsigned long long;
template<typename T>
struct BIT {
#ifndef lowbit
#define lowbit(x) (x & (-x));
#endif
int n;
vector<T> t;
BIT () {}
BIT (int _n): n(_n) {
t.reserve(_n + 1);
t.resize(_n + 1);
}
BIT (int _n, vector<T>& a): n(_n) {
t.resize(_n + 1);
for (int i = 1; i <= n; ++ i) {
t[i] += a[i];
int j = i + lowbit(i);
if (j <= n) t[j] += t[i];
}
}
//单点修改
void update(int i, T x) {
while (i <= n) {
t[i] += x;
i += lowbit(i);
}
}
//区间查询
T sum(int i) {
T ans = 0;
while (i > 0) {
ans += t[i];
i -= lowbit(i);
}
return ans;
}
T query(int l, int r) {
if (l > r) return 0;
return sum(r) - sum(l - 1);
}
//区间修改则存入差分数组,[l, r] + k则update(x,k),update(y+1,-k)
//单点查询则直接求前缀和sum(x)
};
constexpr int N = 1e6;
struct DsuOnTree {
int n, dfn = 0;
vector<int> sz, big, L, R, Node;
vector<vector<int>> g;
//根据题目要求修改
u64 f = 0;
vector<u64> ans, a;
vector<BIT<u64>> y;
DsuOnTree(int n): n(n), sz(n + 1), big(n + 1), L(n + 1), R(n + 1), Node(n + 1) {
g.resize(n + 1);
ans.resize(n + 1);
a.resize(n + 1);
y.resize(3, BIT<u64>(N));
}
void add(int u, int v) {
g[u].emplace_back(v);
g[v].emplace_back(u);
}
void add(int u) {
//计算贡献
auto x = a[u];
f += y[2].query(x + 1, N) - x * y[1].query(x + 1, N);
f += x * x * y[0].sum(x - 1) - x * y[1].sum(x - 1);
y[0].update(x, 1);
y[1].update(x, x);
y[2].update(x, x * x);
}
void del(int u) {
//删除贡献
auto x = a[u];
f -= y[2].query(x + 1, N) - x * y[1].query(x + 1, N);
f -= x * x * y[0].sum(x - 1) - x * y[1].sum(x - 1);
y[0].update(x, -1);
y[1].update(x, -x);
y[2].update(x, -x * x);
}
u64 getAns() {
return f;
}
void dfs0(int u, int fa) {
L[u] = ++dfn;
Node[dfn] = u;
sz[u] = 1;
for (int v : g[u])
if (v != fa) {
dfs0(v, u);
sz[u] += sz[v];
if (!big[u] || sz[big[u]] < sz[v])
big[u] = v;
}
R[u] = dfn;
}
void dfs1(int u, int fa, bool keep) {
// 计算轻儿子的答案
for (int v : g[u])
if (v != fa && v != big[u]) {
dfs1(v, u, false);
}
// 计算重儿子答案并保留计算过程中的数据(用于继承)
if (big[u]) {
dfs1(big[u], u, true);
}
for (int v : g[u])
if (v != fa && v != big[u]) {
// 子树结点的 DFS 序构成一段连续区间,可以直接遍历
for (int i = L[v]; i <= R[v]; i++) {
add(Node[i]);
}
}
add(u);
ans[u] = getAns();
if (keep == false) {
for (int i = L[u]; i <= R[u]; i++) {
del(Node[i]);
}
}
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
DsuOnTree tr(n);
for (int i = 1; i < n; i ++) {
int u, v;
cin >> u >> v;
tr.add(u, v);
}
for (int i = 1; i <= n ; i ++) {
cin >> tr.a[i];
}
tr.dfs0(1, 0);
tr.dfs1(1, 0, 0);
u64 ans = 0;
for (int i = 1; i <= n; i ++) {
ans ^= 2ll * tr.ans[i];
}
cout << ans << '\n';
return 0;
}
序列立方
思路
直接计算很难,考虑将子序列出现次数的立方和转化为在原序列中选出三个序列相同的方案数,这样就可以利用容斥原理计算。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 255, mod = 998244353;
i64 f[N][N][N], sum[N][N][N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i ++)
cin >> a[i];
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= n; j ++) {
for (int k = 1; k <= n; k ++) {
if (a[i] == a[j] && a[j] == a[k])
f[i][j][k] = sum[i - 1][j - 1][k - 1] + 1;
sum[i][j][k] = (sum[i - 1][j][k] + sum[i][j - 1][k] + sum[i][j][k - 1] - sum[i - 1][j - 1][k] - sum[i - 1][j][k - 1] - sum[i][j - 1][k - 1] + sum[i - 1][j - 1][k - 1] + f[i][j][k] + mod + mod + mod) % mod;
}
}
}
cout << sum[n][n][n] << '\n';
return 0;
}
三元环
思路
考虑 \(3\) 个点的有向图,要么成环,要么有一个点入度为 \(2\) ,假设第 个点的入度为 \(d_i\),答案为 \(C_n^3-\sum\limits_{i=1}^nC_{d_i}^2\)。
根据题目关系,\(i\rightarrow j\) 当且仅当 \(i<j \ and\ f_i <f_j \ and\ g_i < g_j\),否则就是 \(j\rightarrow i\),所以根据这个三维关系,我们可以先根据前两维求出 \(i<j\ and\ f_i\ge f_j\) 的入度,然后通过 cdq分治去求满足这个三维关系的各点的度数。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
template<typename T>
struct BIT {
#ifndef lowbit
#define lowbit(x) (x & (-x));
#endif
int n;
vector<T> t;
BIT () {}
BIT (int _n): n(_n) { t.resize(_n + 1); }
BIT (int _n, vector<T>& a): n(_n) {
t.resize(_n + 1);
for (int i = 1; i <= n; ++ i) {
t[i] += a[i];
int j = i + lowbit(i);
if (j <= n) t[j] += t[i];
}
}
//单点修改
void update(int i, T x) {
while (i <= n) {
t[i] += x;
i += lowbit(i);
}
}
//区间查询
T sum(int i) {
T ans = 0;
while (i > 0) {
ans += t[i];
i -= lowbit(i);
}
return ans;
}
T query(int i, int j) {
return sum(j) - sum(i - 1);
}
//区间修改则存入差分数组,[l, r] + k则update(x,k),update(y+1,-k)
//单点查询则直接求前缀和sum(x)
//求逆序对
/*
iota(d.begin(), d.end(), 0);
stable_sort(d.begin(), d.end(), [&](int x, int y) {
return a[x] < a[y];
});去重排序
BIT<i64> tree(n);
i64 ans = 0;
for (int i = 1; i <= n; i ++) {
tree.update(d[i], 1);
ans += i - tree.sum(d[i]);
}
*/
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<array<int, 3>> a(n + 1);
for (int i = 1; i <= n; i ++) {
cin >> a[i][0];
a[i][2] = i;
}
for (int i = 1; i <= n; i ++) {
cin >> a[i][1];
}
BIT<i64> bit(n);
vector<int> in(n + 1);
//求 i < j and fi >= fj
for (int i = n; i >= 1; i --) {
in[i] += bit.sum(a[i][0]);
bit.update(a[i][0], 1);
}
for (int i = n; i >= 1; i --) {
bit.update(a[i][0], -1);
}
auto cdq = [&](auto && self, int l, int r)->void{
if (l == r)
return ;
int mid = l + r >> 1;
self(self, l, mid);
self(self, mid + 1, r);
sort(a.begin() + l, a.begin() + mid + 1, [](auto x, auto y) {
if (x[0] != y[0]) return x[0] < y[0];
return x[1] < y[1];
});
sort(a.begin() + mid + 1, a.begin() + r + 1, [](auto x, auto y) {
if (x[0] != y[0]) return x[0] < y[0];
return x[1] < y[1];
});
//求 i < j and fi < fj and gi < gj
int i = l, j = mid + 1;
while (j <= r) {
while (i <= mid && a[i][0] < a[j][0]) {
bit.update(a[i][1], 1);
i ++;
}
in[a[j][2]] += bit.sum(a[j][1] - 1);
j ++;
}
for (int k = l; k < i; k ++) {
bit.update(a[k][1], -1);
}
//求 i < j and fi < fj and gi >= gj
i = mid, j = r;
while (i >= l) {
while (j > mid && a[j][0] > a[i][0]) {
bit.update(a[j][1], 1);
j --;
}
in[a[i][2]] += bit.sum(a[i][1]);
i --;
}
for (int k = r; k > j; k --) {
bit.update(a[k][1], -1);
}
};
cdq(cdq, 1, n);
i64 ans = 1ll * n * (n - 1) * (n - 2) / 6;
for (int i = 1; i <= n; i ++) {
ans -= 1ll * in[i] * (in[i] - 1) / 2;
}
cout << ans << '\n';
return 0;
}
位运算
思路
考虑绘制真值表:
a | b | c | d | N |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 1 | 1 |
0 | 0 | 1 | 0 | 1 |
0 | 0 | 1 | 1 | 1 |
0 | 1 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
0 | 1 | 1 | 0 | 1 |
0 | 1 | 1 | 1 | 1 |
1 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 1 | 1 |
1 | 0 | 1 | 0 | 1 |
1 | 0 | 1 | 1 | 1 |
1 | 1 | 0 | 0 | 1 |
1 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 0 | 0 |
1 | 1 | 1 | 1 | 1 |
由真值表可知,对于 n 的每一位,如果是 0 则有 4 种方案,是 1 则有 12 种方案,枚举 k 位对应 n 的每一位分别相乘即可。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n, k;
cin >> n >> k;
i64 ans = 1;
for (int i = 0; i < k; i ++) {
if ((n >> i) & 1) ans *= 12;
else ans *= 4;
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
并
思路
点数较少,但坐标很大,考虑离散。
求 k 个矩形面积并集的期望,看到面积并,想到扫描线,但是扫描线只能扫描全部扫,无法单独扫某些矩形,考虑将问题转化。
一个矩形会对 \(H\times W\) 个格子产生贡献,则可以计算 n 个矩形对哪些格子产生贡献,这一步可以用一阶差分二维前缀和计算,假设把 k 个矩形的贡献集中一点,是不是就变成了这个点中 k 个矩形对其产生的期望了呢,现考虑扩展到矩形,要计算 k 个矩形的,就是把这 k 个矩形对点的贡献又统计起来,但这样统计又要在计算的时候去 n 方遍历,诶,之前不是离散了坐标吗,离散后的两个点之间构成的矩形面积就是对当前所覆盖的点产生的总贡献,那我们就可以直接计算当前点被覆盖的次数加上这个矩形的贡献,这样就把所有 k 个矩形产生的贡献集中在一个点被覆盖了 k 次的贡献中了。
设 \(g_i\) 表示被 i 个矩形覆盖的区域的面积之和,考虑枚举选出 k 个矩形,再枚举被覆盖了 i 次的区域,当 \(n-i<k\) 的时候,则说明这个区域至少会被 k 个矩形中的一个所覆盖,那么其产生的贡献就是 \(g_i\),当 \(n-i≥k\) 的时候,不会算,题解是考虑取补集,即考虑这些区域不被 k 个矩形中任意一个覆盖的概率,即 \(\sum_{j=1}^n\Big(1-\frac{C_{n-i}^k}{C_n^k} \Big)\times g_i\)
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
//------取模机------//
using i64 = long long;
template<class T>
constexpr T power(T a, i64 b) {
T res {1};
for (; b; b /= 2, a *= a) {
if (b % 2) {
res *= a;
}
}
return res;
} // 快速幂
constexpr i64 mul(i64 a, i64 b, i64 p) {
i64 res = a * b - i64(1.L * a * b / p) * p;
res %= p;
if (res < 0) {
res += p;
}
return res;
} // 取模乘
template<i64 P>
struct MInt {
i64 x;
constexpr MInt() : x {0} {}
constexpr MInt(i64 x) : x {norm(x % getMod())} {}
static i64 Mod;
constexpr static i64 getMod() {
if (P > 0) {
return P;
} else {
return Mod;
}
}
constexpr static void setMod(i64 Mod_) {
Mod = Mod_;
}//只有P<=0, setMod才生效
constexpr i64 norm(i64 x) const {
if (x < 0) {
x += getMod();
}
if (x >= getMod()) {
x -= getMod();
}
return x;
}
constexpr i64 val() const {
return x;
}
constexpr MInt operator-() const {
MInt res;
res.x = norm(getMod() - x);
return res;
}
constexpr MInt inv() const {
return power(*this, getMod() - 2);
}
constexpr MInt &operator*=(MInt rhs) & {
if (getMod() < (1ULL << 31)) {
x = x * rhs.x % int(getMod());
} else {
x = mul(x, rhs.x, getMod());
}
return *this;
}
constexpr MInt &operator+=(MInt rhs) & {
x = norm(x + rhs.x);
return *this;
}
constexpr MInt &operator-=(MInt rhs) & {
x = norm(x - rhs.x);
return *this;
}
constexpr MInt &operator/=(MInt rhs) & {
return *this *= rhs.inv();
}
friend constexpr MInt operator*(MInt lhs, MInt rhs) {
MInt res = lhs;
res *= rhs;
return res;
}
friend constexpr MInt operator+(MInt lhs, MInt rhs) {
MInt res = lhs;
res += rhs;
return res;
}
friend constexpr MInt operator-(MInt lhs, MInt rhs) {
MInt res = lhs;
res -= rhs;
return res;
}
friend constexpr MInt operator/(MInt lhs, MInt rhs) {
MInt res = lhs;
res /= rhs;
return res;
}
friend constexpr std::istream &operator>>(std::istream &is, MInt &a) {
i64 v;
is >> v;
a = MInt(v);
return is;
}
friend constexpr std::ostream &operator<<(std::ostream &os, const MInt &a) {
return os << a.val();
}
friend constexpr bool operator==(MInt lhs, MInt rhs) {
return lhs.val() == rhs.val();
}
friend constexpr bool operator!=(MInt lhs, MInt rhs) {
return lhs.val() != rhs.val();
}
friend constexpr bool operator<(MInt lhs, MInt rhs) {
return lhs.val() < rhs.val();
}
};
template<>
i64 MInt<0>::Mod = 998244353; //只有P<=0, Mod才生效
constexpr int P = 998244353; //在这设置要用的模数
using Z = MInt<P>;
//------取模机------//
//----计算组合数----//
struct Comb {
int n;
std::vector<Z> _fac; //阶乘
std::vector<Z> _invfac; //阶乘的逆元
std::vector<Z> _inv; //数字的逆元
Comb() : n{0}, _fac{1}, _invfac{1}, _inv{0} {}
Comb(int n) : Comb() {
init(n);
}
void init(int m) {
m = std::min<i64>(m, Z::getMod() - 1);
if (m <= n) return;
_fac.resize(m + 1);
_invfac.resize(m + 1);
_inv.resize(m + 1);
for (int i = n + 1; i <= m; i++) {
_fac[i] = _fac[i - 1] * i;
}
_invfac[m] = _fac[m].inv();
for (int i = m; i > n; i--) {
_invfac[i - 1] = _invfac[i] * i;
_inv[i] = _invfac[i] * _fac[i - 1];
}
n = m;
}
Z fac(int m) {
if (m > n) init(2 * m);
return _fac[m];
}
Z invfac(int m) {
if (m > n) init(2 * m);
return _invfac[m];
}
Z inv(int m) {
if (m > n) init(2 * m);
return _inv[m];
}
Z C(int n, int m) {
if (n < m || m < 0) return 0;
return fac(n) * invfac(m) * invfac(n - m);
}
Z A(int n, int m) {
if (n < m || m < 0 ) return 0;
return fac(n) * invfac(m);
}
} comb;
//----计算组合数----//
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<pair<int, int>> loc1, loc2;
vector<i64> x, y;
for (int i = 0; i < n; i ++) {
int x1, x2, y1, y2;
cin >> x1 >> y1 >> x2 >> y2;
loc1.emplace_back(x1, y1);
loc2.emplace_back(x2, y2);
x.push_back(x1), x.push_back(x2);
y.push_back(y1), y.push_back(y2);
}
sort(x.begin(), x.end());
sort(y.begin(), y.end());
int len1 = unique(x.begin(), x.end()) - x.begin();
int len2 = unique(y.begin(), y.end()) - y.begin();
x.resize(len1);
y.resize(len2);
vector f(len1 + 1, vector<int>(len2 + 1));
for (int i = 0; i < n; i ++) {
auto &[lx, ly] = loc1[i];
auto &[rx, ry] = loc2[i];
lx = lower_bound(x.begin(), x.end(), lx) - x.begin() + 1;
rx = lower_bound(x.begin(), x.end(), rx) - x.begin() + 1;
ly = lower_bound(y.begin(), y.end(), ly) - y.begin() + 1;
ry = lower_bound(y.begin(), y.end(), ry) - y.begin() + 1;
f[lx][ly] ++ , f[lx][ry] --, f[rx][ly] --, f[rx][ry] ++;
}
for (int i = 1; i <= len1; i ++)
for (int j = 1; j <= len2; j ++)
f[i][j] += f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1];
vector<Z> num(n + 1);
for (int i = 1; i <= len1; i ++)
for (int j = 1; j <= len2; j ++)
num[f[i][j]] += Z(x[i] - x[i - 1]) * (y[j] - y[j - 1]);
for (int k = 1; k <= n; k ++) {
Z ans = 0;
for (int i = 0; i <= n; i ++) {
ans += (comb.C(n, k) - comb.C(n - i, k)) * num[i];
}
ans /= comb.C(n, k);
cout << ans << '\n';
}
return 0;
}