题1
前言:不一定是好题。
abc359G
一道入门的树上启发式合并题,考虑拆贡献,设树的根节点为 \(1\),以节点 \(u\) 为根的子树内颜色为 \(x\) 的点的个数为 \(c_{u, x}\), 节点 \(u\) 连向儿子 \(v\) 的边对颜色 \(x\) 的贡献为 \(c_{v,x} \times (c_{1,x} - c_{v,x})\),则我们只要记录每个点为根的子树中所有颜色的出现次数即可,
但是直接做显然是 \(\mathcal{O}(n^2)\) 的(这里记颜色总数为 \(\mathcal{O}(n)\))。如何去优化这一过程呢?
树上启发式合并,启发式合并是基于人类的经验和直观感觉,对一些算法的优化。最简单的启发式合并就是并查集的启发式合并: 合并时将较小的集合合并到较大的集合中,这样可以使查找操作变为 \(\mathcal{O}(\log n)\).而树上启发式合并也可以理解成这样的过程,即把一个点的所有儿子的子树在这个点处按顺序启发式合并,相当于依次把每个儿子的子树接在这个点上,那么每次集合大小会 \(\times 2\),那均摊下来仍然是每次 \(\mathcal{O}(\log n)\) 的.
然后可以做一个小优化,就是每个点子树内,每种颜色的点组成的集合只用一个数字代替,那相当于做了一个去重的工作,方便查找而且时间复杂度肯定是更优的.
具体可以用 map 实现,这样在交换的时候交换两个 map 数组是 \(\mathcal{O}(1)\) 的.
Code:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
typedef long long ll;
typedef pair<int, int> pii;
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cerr << arg << ' ';
dbg(args...);
}
int n, c[N], a[N];
ll val[N], ans;
vector<int>e[N];
unordered_map<int, ll>mp[N];
inline void dfs(int u, int fa) {
mp[u][a[u]] = 1;
val[u] = c[a[u]] - 1;
//这里是因为所有和 u 颜色一样的点到 u 的路径上肯定有恰好一条边和 u 相连,故有 1 的贡献
for (auto v : e[u]) if (v != fa) {
dfs(v, u);
if (mp[u].size() < mp[v].size()) {
swap(mp[u], mp[v]); // 这里的 swap 是 O(1) 的
swap(val[u], val[v]);
}
for (auto [x, y] : mp[v]) {
val[u] -= mp[u][x] * (c[x] - mp[u][x]);
mp[u][x] += y;
val[u] += mp[u][x] * (c[x] - mp[u][x]);
}
}
ans += val[u];
}
int main() {
scanf("%d", &n);
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
e[u].push_back(v); e[v].push_back(u);
}
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]); c[a[i]]++;
}
dfs(1, 0);
printf("%lld\n", ans);
return 0;
}
abc368E
题意:给定 \(M\) 辆火车的开车时间 \(S_i\),到达时间 \(T_i\),开车地点 \(A_i\),到达地点 \(B_i\),如果两个火车 \(i, j\) 满足 \(B_i=A_j\),且 \(T_i \le S_j\),那么就会有人从 \(i\) 换乘到 \(j\),假设每辆火车都晚出发(也会晚到相同的时间)一段时间,记为 \(X_i\),给定 \(X_1\),求最小的 \(X_2+X_3+X_4+\dots+X_M\),使原本能换乘的两辆车还能换乘.实际意义就是如果一辆车要延误求别的车最少延误的时间总和.
Solution 1
贪心,要使 \(X_2+X_3+X_4+\dots+X_M\) 最小,则要 \(X_i\) 最小,按时间顺序考虑车的顺序,\([time_i,type_i,id_i]\) 分别表示第 \(i\) 个事件的时间,出发 (\(0\)) 还是到达 (\(1\)),\(id_i\) 表示车的编号,假设 \(P_i\) 是当前最晚到达站点 \(i\) 的时刻,为什么要记这个?因为 \(type_i=0\) 时,要保证正常发车,需要 \(id_i\) 真正到达的时间 \(\ge P_{A_{id_x}}\),即 \(X_{id_i}=P_{B_{id_i}}-time_i\),且 \(type_i=0\) 时,\(P_{B_{id_x}}\) 可以被 \(time_i+X_{id_i}\) 更新.
按上述所说更新即可,这里更新指取 \(\max\).
Code:
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, mod = 998244353;
typedef long long ll;
typedef pair<int, int> pii;
void cmax(int &x, int y) { if (x < y) x = y; }
void cmin(int &x, int y) { if (x > y) x = y; }
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
void mul(int &x, int y) { x = 1ll * x * y % mod; }
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cerr << arg << ' ';
dbg(args...);
}
int n, m, tot;
ll x[N], p[N];
struct Train {
ll a, b, s, t;
} c[N];
struct Event {
ll t, type, id;
bool operator < (const Event &A) const {
return t == A.t ? (type == A.type ? id < A.id : type > A.type) : t < A.t; // 注意,同一时刻先看到达的
}
} e[N]; //注意这里空间是 m * 2
int main() {
scanf("%d%d%lld", &n, &m, &x[1]);
for (int i = 1; i <= m; i++) {
auto &[a, b, s, t] = c[i];
scanf("%lld%lld%lld%lld", &a, &b, &s, &t);
e[++tot] = {s, 0, i}; e[++tot] = {t, 1, i};
}
sort(e + 1, e + tot + 1);
for (int i = 1; i <= tot; i++) {
auto [t, type, id] = e[i];
if (!type) {
if (id > 1) x[id] = max(0ll, p[c[id].a] - t); //每个 id 这里的更新只会进行一次
} else {
p[c[id].b] = max(p[c[id].b], t + x[id]); // 这里有多次
}
}
for (int i = 2; i <= m; i++) printf("%lld\n", x[i]);
return 0;
}
Solution 2
不按时间考虑,考虑火车 \(i, j\),若 \(B_j=A_i\),\(T_j \le S_i\),需满足 \(T_j+X_j \le S_i+X_i\),移项得 \(X_i=T_j+X_j-S_i\),对于所有满足条件的 \(j\) 取 \(\max\),树状数组维护即可,注意可以把 \(B_j=x\) 的单独开一个树状数组,用 unordered_map 充当数组.
要按 \(S_i\) 从小到大排序,因为这样能保证所有 \(T_j \le S_i\) 的车都已经更新过了(因为 \(T_j \le S_i, S_j < T_j\), 可得 \(S_j < S_i\)),即 \(X_j\) 已知.
代码不放了.
虽然没人看,但还是为这么久没记录致歉。
CF1852B
个人认为,构造题比较重要的是性质的挖掘,看到这题,首先发现可以直接按将 \(a\) 排序,这时发现 \(b\) 也同时被排序了,并且由于要从 \([-n, -1]\) 和 \([1, n]\) 之间选 \(n\) 个数,不能选相反数,所以 \((-1, 1), (-2, 2) \cdots (-n, n)\) 这 \(n\) 对数中每对只能选一个数,而当 \(a_n=n\) 时 \(b_n\) 必定是 \(n\), \(a_1=0\) 时 \(b_1\) 必定是 \(-n\), 但是由上知二者不能同时成立,如果只有一个成立那么可以分解子问题,如果 \(a_n=n\) 则让 \(a_i \leftarrow a_i - 1\).
时间复杂度 \(\mathcal{O}(n \log n)\).
ARC179B
乍一看明显是状压,但是怎么维护可行性?考虑如果放了一个 \(j\), 那么接下来所有满足 \(X_i=j\) 的都是可以放的,但是 \(j\) 不能放了(除非 \(X_j=j\)),所以直接状压,\(dp_{i,s}\) 表示考虑到前 \(i\) 个数,能放的数的状态为 \(s\) 的方案数。转移显然.
时间复杂度 \(\mathcal{O}(nm2^m)\).
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 1e4 + 10, M = 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cout << arg << ' ';
dbg(args...);
}
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
int n, m, mask[N], dp[N][1 << M]; // mask[i]: 满足 x_j = i 的 j 的集合 dp[i][s]: 考虑到前 i 位,能放的数集合为 j 的方案数
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> m >> n;
for (int i = 0, x; i < m; i++) {
cin >> x; x--;
mask[x] |= (1 << i);
}
dp[0][(1 << m) - 1] = 1;
for (int i = 0; i < n; i++) {
for (int s = 0; s < (1 << m); s++) {
for (int j = 0; j < m; j++) if (s >> j & 1) {
add(dp[i + 1][s ^ (1 << j) | mask[j]], dp[i][s]);
}
}
}
int ans = 0;
for (int s = 0; s < (1 << m); s++) add(ans, dp[n][s]);
cout << ans << '\n';
return 0;
}
dmy D6T1

容易证明如果一个数如果出现则每个子段都要出现,所以每一段的 \(\mathrm{MEX}\) 都是全局 \(\mathrm{MEX}\), 双指针 + dp 即可。
时间复杂度 \(\mathcal{O}(n)\).
dmy D6T2

\(|s|, |t| \le 100\)
观察到操作具有可逆性,所以可以考虑构造等价类,构造出后只需要让 \(s\) 和 \(t\) 都变等价类再将 \(t\) 的过程倒序即可:
对于长度为奇数:每次找到一个极长的相同段(和上一次不能是相同的,长度为偶数),因为是一个环,所以肯定能找到,直到变成全 \(0\) 或全 \(1\),注意不会出现 \(s\) 变成全 \(0\) 但 \(t\) 变成全 \(1\) 的情况,因为如果 \(1\) 个数的奇偶性不同是肯定没法构造的,反之也是同理的。
对于长度为偶数:考虑隔一位翻转(不是真的翻转),然后原本相同的就会变成不同的,然后相同翻转的这个操作就等价于 swap 一对 \(0, 1\), 然后等价类就可以类似冒泡排序一样,构造成有序的(\(000\cdots11111111\)),容易证明有解则会构造出相同的等价类。
操作次数 \(\mathcal{O}(n^2)\).
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 2e5 + 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cout << arg << ' ';
dbg(args...);
}
int n, a[N], b[N], t1, t2;
char s[N], t[N];
void solve1() {
int mx = 0, p = -1;
while (true) {
int c0 = 0, c1 = 0;
for (int i = 0; i < n; i++) c0 += (s[i] == '0'), c1 += (s[i] == '1');
if (!c0 || !c1) break;
mx = 0;
for (int i = 0; i < n; i++) {
int j = (i + 1) % n, cnt = 1;
while (cnt <= n && s[j] == s[i]) cnt++, j = (j + 1) % n;
if (cnt & 1) cnt--;
if (i != p && cnt > mx) mx = cnt, p = i; // 不能和上次找到同一段
}
int j = (p + 1) % n, cnt = 1;
a[++t1] = p;
while (cnt < mx && s[j] == s[p]) {
if (cnt % 2 == 0) a[++t1] = j;
cnt++, j = (j + 1) % n;
}
for (int i = p; i != j; i = (i + 1) % n) s[i] = !(s[i] - '0') + '0';
}
mx = 0, p = -1;
while (true) {
int c0 = 0, c1 = 0;
for (int i = 0; i < n; i++) c0 += (t[i] == '0'), c1 += (t[i] == '1');
if (!c0 || !c1) break;
mx = 0;
for (int i = 0; i < n; i++) {
int j = (i + 1) % n, cnt = 1;
while (cnt <= n && t[j] == t[i]) cnt++, j = (j + 1) % n;
if (cnt & 1) cnt--;
if (i != p && cnt > mx) mx = cnt, p = i;
}
int j = (p + 1) % n, cnt = 1;
b[++t2] = p;
while (cnt < mx && t[j] == t[p]) {
if (cnt % 2 == 0) b[++t2] = j;
cnt++, j = (j + 1) % n;
}
for (int i = p; i != j; i = (i + 1) % n) t[i] = !(t[i] - '0') + '0';
}
cout << t1 + t2 << '\n';
for (int i = 1; i <= t1; i++) cout << a[i] << '\n';
for (int i = t2; i; i--) cout << b[i] << '\n';
}
void solve2() {
for (int i = 1; i < n; i += 2) s[i] = !(s[i] - '0') + '0', t[i] = !(t[i] - '0') + '0';
bool fl = 1;
while (true) {
fl = 0;
for (int i = 0; i < n - 1; i++) if (s[i] > s[i + 1]) {
a[++t1] = i;
swap(s[i], s[i + 1]);
fl = 1;
}
if (!fl) break;
}
fl = 1;
while (true) {
fl = 0;
for (int i = 0; i < n - 1; i++) if (t[i] > t[i + 1]) {
b[++t2] = i;
swap(t[i], t[i + 1]);
fl = 1;
}
if (!fl) break;
}
cout << t1 + t2 << '\n';
for (int i = 1; i <= t1; i++) cout << a[i] << '\n';
for (int i = t2; i; i--) cout << b[i] << '\n';
}
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> s >> t; n = strlen(s);
if (n & 1) solve1();
else solve2();
return 0;
}
dmy D6T3

\(n \le 2 \times 10^5\)
字典序最小启示我们逐位确定,假设当前为是左括号后面全部是有括号能否满足,可以证明如果有方案一定可以构造出来(但是我不太会证,感性理解是简单的),左括号当 \(1\) 右括号当 \(-1\) 使用线段树维护即可。
时间复杂度 \(\mathcal{O}(n \log n)\).
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 4e5 + 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
cout << arg << ' ';
dbg(args...);
}
int n, las[N], nxt[N];
char ans[N];
struct SegTree {
#define ls (u << 1)
#define rs (u << 1 | 1)
#define mid (l + r >> 1)
int tr[N << 2], tag[N << 2];
void pushup(int u) { tr[u] = max(tr[ls], tr[rs]); }
void pushdown(int u) {
if (!tag[u]) return ;
tr[ls] += tag[u]; tr[rs] += tag[u]; tag[ls] += tag[u]; tag[rs] += tag[u];
tag[u] = 0;
}
void build(int l = 1, int r = n, int u = 1) {
if (l == r) {
tr[u] = -(n - l + 1);
return;
}
build(l, mid, ls); build(mid + 1, r, rs);
pushup(u);
}
void add(int x, int y, int v, int l = 1, int r = n, int u = 1) {
if (x <= l && r <= y) { tr[u] += v; tag[u] += v; return ; }
pushdown(u);
if (x <= mid) add(x, y, v, l, mid, ls);
if (mid < y) add(x, y, v, mid + 1, r, rs);
pushup(u);
}
} st;
bool check() {
int top = 0;
for (int i = 1; i <= n; i++) {
if (ans[i] == '(') top++;
else top--;
if (top < 0) return 0;
}
return 1;
}
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n;
n <<= 1;
for (int i = 1, x; i <= n; i++) {
cin >> x;
if (!las[x]) las[x] = i;
else nxt[las[x]] = i;
}
st.build();
for (int i = 1; i <= n; i++) if (nxt[i]) {
st.add(1, i, 2); st.add(1, nxt[i], 2);
if (st.tr[1] > 0) {
st.add(1, i, -2); st.add(1, nxt[i], -2);
ans[i] = ans[nxt[i]] = ')';
} else ans[i] = ans[nxt[i]] = '(';
}
if (!check()) cout << "(\n";
else {
for (int i = 1; i <= n; i++) cout << ans[i];
cout << '\n';
}
return 0;
}
接下来是我做过的紫的题解。
P2087
这种集合的形式一看就很能哈希,直接考虑给每个人随机权值,一个集合的权值就是所有人权值的和,然后注意到不同的集合最多有 \(\mathcal{O}(n + q)\) 个,所以直接用 set 维护每个房间是否已经做过实验,如果已经做过就删掉,每次更新的时候修改一下权值和 set 就好了。
时间复杂度 \(\mathcal{O}(q \log n)\).
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int N = 1e5 + 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
cout << arg << ' ';
dbg(args...);
}
mt19937 rnd(11);
int n, m, Q, bel[N], sz[N];
ull val[N], s[N];
set<int>st;
unordered_map<ull, int>mp;
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m >> Q;
for (int i = 1; i <= n; i++) {
bel[i] = 1;
val[i] = (ull)rnd() * rnd();
s[1] += val[i]; sz[1]++;
}
st.insert(1);
while (Q--) {
char op; int x, y;
cin >> op >> x >> y;
if (op == 'C') {
if (bel[x] == y) continue;
st.erase(bel[x]); st.erase(y); s[bel[x]] -= val[x]; s[y] += val[x]; sz[bel[x]]--; sz[y]++;
if (!mp.count(s[bel[x]])) st.insert(bel[x]);
if (!mp.count(s[y])) st.insert(y);
bel[x] = y;
} else {
int ans = 0;
for (auto it = st.lower_bound(x); it != st.end() && (*it) <= y; it = st.lower_bound(x)) {
ans += sz[*it]; mp[s[*it]] = 1; st.erase(it);
}
cout << ans << '\n';
}
}
return 0;
}
P2150
有一个性质:一个数 \(n\) 大于 \(\sqrt n\) 的质因子最多只有 \(1\) 个,于是把这个拿出来之后剩下的质因子只有 \(2, 3, 5, 7, 11, 13, 17, 19\) 这 \(8\) 个,对其进行状压,在最大质因子相同的段上跑 dp,考虑 \(a_i\) 放在哪里即可。
时间复杂度 \(\mathcal{O}(n2^{2\pi(\sqrt{n})})\).
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 5e2 + 10, M = 256;
const int p[8] = {2, 3, 5, 7, 11, 13, 17, 19};
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
cout << arg << ' ';
dbg(args...);
}
int n, mod, f[M][M], g[M][M], dp[M][M];
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
struct Factor {
int x, big, s;
void init() {
big = -1;
for (int i = 0; i < 8; i++) {
if (x % p[i] == 0) {
s |= (1 << i);
while (x % p[i] == 0) x /= p[i];
}
}
if (x != 1) big = x;
}
bool operator < (const Factor &A) const {
return big < A.big;
}
} a[N];
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> mod;
for (int i = 1; i < n; i++) {
a[i].x = i + 1; a[i].init();
}
sort(a + 1, a + n);
dp[0][0] = 1;
for (int i = 1; i < n; i++) {
if (i == 1 || a[i].big != a[i - 1].big || a[i].big == -1) {
memcpy(f, dp, sizeof f); memcpy(g, dp, sizeof g);
}
for (int j = M - 1; j >= 0; j--) {
for (int k = M - 1; k >= 0; k--) if (!(j & k)) {
if (!(a[i].s & k)) add(f[j | a[i].s][k], f[j][k]);
if (!(a[i].s & j)) add(g[j][k | a[i].s], g[j][k]);
}
}
if (i == n - 1 || a[i].big != a[i + 1].big || a[i].big == -1) {
for (int j = 0; j < M; j++) {
for (int k = 0; k < M; k++) {
dp[j][k] = ((f[j][k] + g[j][k]) % mod - dp[j][k] + mod) % mod;
}
}
}
}
int ans = 0;
for (int i = 0; i < M; i++) for (int j = 0; j < M; j++) add(ans, dp[i][j]);
cout << ans << '\n';
return 0;
}
P3763
修改次数只有 \(3\) 所以每次找到第一个要修改的位置修改,只会重复不超过 \(3\) 次,对于每个子串二分 + 哈希暴力 check 即可。
时间复杂度 \(\mathcal{O}(n \log n)\).
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int N = 1e5 + 10, base = 61, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cout << arg << ' ';
dbg(args...);
}
int T, n, m;
char s[N], t[N];
ull h1[N], h2[N], pw[N];
ull get1(int l, int r) {
return h1[r] - h1[l - 1] * pw[r - l + 1];
}
ull get2(int l, int r) {
return h2[r] - h2[l - 1] * pw[r - l + 1];
}
bool check(int x, int y) {
int fail = 0, pos = 1;
while (x <= y) {
int L = x - 1, R = y + 1;
while (L + 1 < R) {
int mid = (L + R) >> 1;
if (get1(x, mid) == get2(pos, pos + mid - x)) L = mid;
else R = mid;
}
if (R == y + 1) return 1;
fail++;
pos += R - x + 1; x = R + 1;
if (fail > 3) return 0;
}
return 1;
}
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
pw[0] = 1;
for (int i = 1; i < N; i++) pw[i] = pw[i - 1] * base;
cin >> T;
while (T--) {
cin >> (s + 1) >> (t + 1);
n = strlen(s + 1), m = strlen(t + 1);
int ans = 0;
for (int i = 1; i <= n; i++) h1[i] = h1[i - 1] * base + s[i];
for (int i = 1; i <= m; i++) h2[i] = h2[i - 1] * base + t[i];
for (int i = 1; i <= n - m + 1; i++) {
ans += check(i, i + m - 1);
}
cout << ans << '\n';
}
return 0;
}
P3959
考虑最后的图一定是树,然后按照深度状压 dp,预处理 \(f_{s, t}\) 表示 \(s\) 集合已经在图中,要使 \(t\) 集合也在集合中连边所需要的最小代价即可。
时间复杂度 \(\mathcal{O}(n 3^n)\).
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 12, M = 4096, inf = 5e5 + 10;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cout << arg << ' ';
dbg(args...);
}
int n, m, all, a[N][N], f[M][M], dp[M][M], nxt[M];
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) a[i][j] = inf;
for (int i = 0, u, v, w; i < m; i++) {
cin >> u >> v >> w; --u; --v;
a[u][v] = min(a[u][v], w); a[v][u] = min(a[v][u], w);
}
all = (1 << n) - 1;
for (int s = 0; s <= all; s++) {
int U = all ^ s, p = 0;
for (int t = U; t; t = (t - 1) & U) {
nxt[t] = p; p = t;
}
for (int t = p; t; t = nxt[t]) {
int i = __builtin_cta(t), val = inf;
for (int j = 0; j < n; j++) if (s >> j & 1) {
val = min(val, a[i][j]);
}
f[s][t] = f[s][t ^ (1 << i)] + val
}
}
for (int i = 0; i < n; i++) dp[0][1 << i] = 0;
for (int d = 1; d < n; d++) {
for (int s = 0; s <= all; s++) {
for (int t = s; t; t = (t - 1) & s) {
dp[d][s] = min(dp[d][s], dp[d - 1][s ^ t] + f[s ^ t][t] * i);
}
}
}
int ans = 1 << 30;
for (int i = 0; i < n; i++) ans = min(ans, dp[i][all]);
cout << ans << '\n';
return 0;
}
P3586
将所有 \(>s\) 的数推平成 \(s\) 就变成了全局和是否 \(\ge c \times s\), 树状数组维护即可。
CF808E
感觉是一道非常有意思的题。
首先先对重量相同的,按价格降序排序。
Sol1
考虑枚举选出的重量为 \(3\) 的数字个数,那么就转化为了每次选 \(1\) 或 \(2\) 使得价格最大。
然后是两种思路:
- 考虑如果选了偶数个 1,那就两两打包成 2,选奇数个就强制把第一个选了再两两打包,然后将打包后所有 \(2\) 排序,贪心取即可。
- 直接 dp。
Sol2
枚举完 \(3\) 的个数 \(x\) 之后相当于就是在求 \(\max\limits_{i \times 2 \le m - x \times 3}s_{1,i}+s_{2,\lfloor\frac{m - x \times 3 - i}{2}\rfloor}\), 其中 \(s_1,s_2\) 是 \(1, 2\) 的前缀和。然后这个东西是单峰的,三分即可。
Sol3
神秘 dp, 直接记当前最优解时会分别取多少个 1/2/3,正常转移会 wa, 但只要加上 \(f_i \leftarrow f_{i-2} - 最后的 1 + 最后的 3\) 就是对的,证明不会,实在想不出来把所有可能的 \(6\) 个转移方程列出来就行。
时间复杂度 \(\mathcal{O}(n \log n)\).
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
typedef long long ll;
typedef pair<int, int> pii;
template <class T>
void cmax(T &x, T y) { if (x < y) x = y; }
int n, m, v[N], w[N];
vector<int>a[5];
pair<ll, array<int, 3>> dp[N];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &v[i], &w[i]);
a[v[i]].push_back(w[i]);
}
for (int i = 1; i <= 3; i++) sort(a[i].begin(), a[i].end(), greater<int>());
dp[0] = {0, {0, 0, 0}};
for (int i = 0; i <= m; i++) {
auto [c, p] = dp[i]; auto [x, y, z] = p;
if (x < a[1].size()) cmax(dp[i + 1], {c + a[1][x], {x + 1, y, z}});
if (y < a[2].size()) cmax(dp[i + 2], {c + a[2][y], {x, y + 1, z}});
if (z < a[3].size()) cmax(dp[i + 3], {c + a[3][z], {x, y, z + 1}});
if (x && z < a[3].size()) cmax(dp[i + 2], {c - a[1][x - 1] + a[3][z], {x - 1, y, z + 1}});
}
ll ans = 0;
for (int i = 0; i <= m; i++) ans = max(ans, dp[i].first);
printf("%lld\n", ans);
return 0;
}
CF908G
考虑把一个数位排序后的数拆成若干个 \(11..11\) 相加的形式,如 \(3556\) 可以表示为
显然第 \(i\) 行 \(1\) 的个数等于这个数 \(\ge i\) 的数位个数,显然至多有 \(9\) 行,对于每行 \(1\) 的个数进行数位 dp 即可。
时间复杂度 \(\mathcal{O}(n^2D)\), 其中 \(D = 10\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 7e2 + 10, mod = 1e9 + 7;
typedef long long ll;
typedef pair<int, int> pii;
template <class T>
void add(T &x, T y) { x = (x + y) % mod; }
char s[N];
int c[N], len;
ll dp[N][N][10];
ll dfs(int x, int cnt, bool lim, int d, int p) {
if (x == 0) return cnt == p;
if (cnt > p) return 0;
if (!lim && ~dp[x][p - cnt][d]) return dp[x][p - cnt][d];
int up = lim ? c[x] : 9;
ll res = 0;
for (int i = 0; i <= up; i++) add(res, dfs(x - 1, cnt + (i >= d), lim && (i == up), d, p));
if (!lim) dp[x][p - cnt][d] = res;
return res;
}
ll solve(char s[]) {
len = strlen(s + 1);
for (int i = 1; i <= len; i++) c[i] = s[len - i + 1] - '0';
ll res = 0;
for (int d = 1; d <= 9; d++) {
ll base = 1;
for (int i = 1; i <= len; i++) {
add(res, base * dfs(len, 0, 1, d, i) % mod);
base = (base * 10 + 1) % mod;
}
}
return res;
}
int main() {
memset(dp, 255, sizeof dp);
scanf("%s", s + 1);
printf("%lld\n", solve(s));
return 0;
}
CF55D
和普通数位 dp 的唯一不同是要记所有数位的 \(\mathrm{lcm}\),最后判断是否整除,记忆化数组就离散化一下即可。
CF762D
只走上下右就是方格取数,转移显然。
考虑向左走,如果连续向左,取到的一定是一个矩形中的所有数,而这个矩形可以通过上下右走和至多一次向左走取到,可以理解成存在一种等价形式,而这一次向左走只有可能是在第一行时走 “右、下、左、下、右” 或者在第三行时走“右、上、左、上、右”。有了这个结论就可以直接 dp 了。
时间复杂度 \(\mathcal{O}(n)\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
typedef long long ll;
typedef pair<int, int> pii;
int n, a[5][N];
ll dp[5][N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= 3; i++) for (int j = 1; j <= n; j++) scanf("%d", &a[i][j]);
for (int i = 1; i <= 3; i++) for (int j = 0; j <= n; j++) dp[i][j] = -(1ll << 60);
dp[1][0] = 0;
for (int i = 1; i <= n; i++) {
dp[1][i] = max(dp[1][i - 1], max(dp[2][i - 1] + a[2][i], dp[3][i - 1] + a[3][i] + a[2][i])) + a[1][i];
dp[2][i] = max(dp[1][i - 1] + a[1][i], max(dp[2][i - 1], dp[3][i - 1] + a[3][i])) + a[2][i];
dp[3][i] = max(dp[1][i - 1] + a[1][i] + a[2][i], max(dp[2][i - 1] + a[2][i], dp[3][i - 1])) + a[3][i];
if (i > 1) {
ll sum = 0;
for (int j = 1; j <= 3; j++) sum += a[j][i] + a[j][i - 1];
dp[1][i] = max(dp[1][i], dp[3][i - 2] + sum);
dp[3][i] = max(dp[3][i], dp[1][i - 2] + sum);
}
}
printf("%lld\n", dp[3][n]);
return 0;
}
CF660E
首先计算空序列,有 \(m^n\) 种,接下来考虑非空序列,只考虑这个子序列第一次出现的位置,假设分别为 \(pos_{1, \cdots, i}\), 值分别为 \(val_{1, \cdots, i}\) 那么要求在 \((pos_j, pos_{j+1})\) 之间不存在 \(val_j\)(特别地,\((0, pos_1)\) 之间不能有 \(val_1\)), 所以 \([1, pos_i]\) 中除了 \(pos\) 的位置,都有 \(m-1\) 种方案,剩下的 \(n - pos_i\) 共有 \(m\) 种。得到下面的式子:
其中 \(i\) 为长度,\(j\) 为 \(pos_i\), \(m^i\) 是在选子序列,\(\binom{i - 1}{j - 1}\) 是在选 \(pos_{1, \cdots, i-1}\)。
考虑化简:
时间复杂度 \(\mathcal{O}(n \log n)\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
typedef long long ll;
typedef pair<int, int> pii;
ll n, m;
ll qpow(ll a, ll b) {
a %= mod;
ll res = 1;
for (; b; b >>= 1, a = a * a % mod) if (b & 1) res = res * a % mod;
return res;
}
int main() {
scanf("%lld%lld", &n, &m);
ll ans = qpow(m, n); //空串
for (int i = 0; i < n; i++) ans = (ans + qpow(m, n - i) * qpow(2 * m - 1, i) % mod) % mod;
printf("%lld\n", ans);
return 0;
}
CF1342F
考虑怎么刻画当前的信息:最后的序列中每一个 \(a_j\) 都被若干个数加过,这些数就形成了几个集合 \(S_1, S_2, \cdots\), 这些集合中所有元素的和是单调递增的,且存在 \(p_1 \in S_1, p_2 \in S_2, \cdots\) 且 \(p\) 单调递增,表示把 \(S_i\) 中所有元素加到了 \(p_i\) 上。那么一次操作相当于合并两个集合,最少操作次数等价于最多划分的集合数。
考虑朴素的 dp,\(f_{i, j, k}\) 表示考虑集合已经选了的数的集合为 \(i\),最后一个划分的集合中的 \(p\) 最小为 \(j\),和为 \(k\),最多划分多少个集合。这显然过不去,然后发现值域很小(\(f_{i, j, k}\) 最大为 \(n\)),可以考虑交换值域和 \(k\) 这一维,显然这是对的。
所以现在 \(f_{i, j, k}\) 表示考虑集合已经选了的数的集合为 \(i\),最后一个划分的集合中的 \(p\) 最小为 \(j\),划分了 \(k\) 个集合,和最小是多少,转移显然。
时间复杂度 \(\mathcal{O}(n^23^n)\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 16, inf = 1 << 30;
typedef long long ll;
typedef pair<int, int> pii;
int T, n, a[N], dp[N][N][1 << N], sum[1 << N], id[N];
pii pre[N][N][1 << N];
int main() {
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]), id[i] = i + 1;
for (int i = 0; i < (1 << n); i++) {
sum[i] = 0;
for (int j = 0; j <= n; j++)
for (int k = 0; k <= n; k++) dp[j][k][i] = inf;
for (int j = 0; j < n; j++) if (i & (1 << j)) sum[i] += a[j];
}
dp[0][0][0] = 0;
for (int msk = 0; msk < (1 << n); msk++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (dp[i][j][msk] == inf) continue;
int rmsk = msk ^ ((1 << n) - 1);
for (int nmsk = rmsk; nmsk > 0; nmsk = (nmsk - 1) & rmsk) {
if (sum[nmsk] <= dp[i][j][msk] || (!(nmsk >> j))) continue;
int pos = __builtin_ctz(nmsk >> j) + j;
if (sum[nmsk] < dp[i + 1][pos][msk | nmsk]) {
dp[i + 1][pos][msk | nmsk] = sum[nmsk];
pre[i + 1][pos][msk | nmsk] = {j, msk};
}
}
}
}
}
int ans = 0, pos = 0, msk = (1 << n) - 1;
for (int i = 1; i <= n; i++) for (int j = 0; j < n; j++) if (dp[i][j][(1 << n) - 1] != inf) ans = i;
for (int i = 0; i < n; i++) if (dp[ans][i][(1 << n) - 1] != inf) pos = i;
vector<pii>res;
for (int i = ans; i; i--) {
auto [npos, nmsk] = pre[i][pos][msk];
int rmsk = nmsk ^ msk;
for (int j = 0; j < n; j++) if (j != pos && (rmsk & (1 << j))) {
res.push_back({id[j], id[pos]});
for (int k = j + 1; k < n; k++) id[k]--;
}
pos = npos, msk = nmsk;
}
printf("%d\n", res.size());
for (auto [x, y] : res) printf("%d %d\n", x, y);
}
return 0;
}
AT_agc022_e
考虑维护一个栈,要求用尽量少的栈空间存下当前状态是否合法,所以我们要尽量压缩一个字符串的长度,让一些字符串压缩后拥有相同的状态,并且方便转移。如果我们得到了一个压缩方式,那么我们就可以用 dp 来模拟压缩,最后根据各个状态求得答案。
考虑当前 push 进栈的元素:
如果是 \(1\):若栈顶是 \(0\) 直接抵消,因为无论后面加了 \(0\) 还是 \(1\), 中位数都是后面加的数,如果是 \(1\) 那肯定合法,原因下面说,直接忽略这个 \(1\) 即可,如果栈为空就加入一个 \(1\)。
如果是 \(0\):若栈顶元素有两个 \(0\) 那么抵消成一个肯定不劣,否则往栈顶加一个 \(0\)。
这样做的道理是什么?
发现这样的压缩方式可以使得 \(1\) 都在 \(0\) 下方(如果在上面会抵消),并且栈中最多有 \(2\) 个 \(1\) 和 \(2\) 个 \(0\),这样的压缩如果合法原串显然合法,不合法原串也肯定不合法,并且判断压缩后的串是否合法十分简单,显然只要看 \(1\) 的个数是否 \(\ge 0\) 的个数即可。
时间复杂度 \(\mathcal{O}(n)\)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10, mod = 1e9 + 7;
typedef long long ll;
typedef pair<int, int> pii;
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cerr << arg << ' ';
dbg(args...);
}
int n;
int dp[N][3][3]; //前 i 位, 0 有 j 个, 1 有 k 个
char s[N];
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
dp[0][0][0] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 3; k++) if (dp[i][j][k]) {
if (s[i + 1] != '0') {
if (j) add(dp[i + 1][j - 1][k], dp[i][j][k]);
else add(dp[i + 1][j][min(k + 1, 2)], dp[i][j][k]);
}
if (s[i + 1] != '1') {
if (j == 2) add(dp[i + 1][1][k], dp[i][j][k]);
else add(dp[i + 1][j + 1][k], dp[i][j][k]);
}
}
}
}
int ans = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j <= i; j++) {
add(ans, dp[n][j][i]);
}
}
printf("%d\n", ans);
return 0;
}
P7315
首先题意可以转化为用若干次 \(1, 2\) 操作使得矩阵中至多 \(k\) 个 \(1\), 并且操作顺序是无所谓的。
乍一看不可做,观察一下有什么性质?\(k \le n\) 非常重要。
先考虑 \(k < n\), 因为此时根据抽屉原理,肯定有一行会在 \(1, 2\) 操作完后全部变成 \(0\), 考虑枚举这一行,就可以得出每一列是否操作,列操作序列就等于这一行的序列(不用考虑这一行是否操作,因为这一行翻转了相当于别的所有行都翻转),将别的行用列操作翻转后,不进行操作就需要 \(1\) 的个数次操作 \(3\), 否则需要 \(0\) 的个数次操作 \(3\), 取 \(\mathrm{min}\) 后判断总操作次数是否 \(\le k\) 即可。
然后考虑 \(k = n\), 此时上面的策略也是成立的,但是多了一种情况:操作 \(1, 2\) 完成后,每行恰好有一个 \(1\), 这样正好是 \(n\) 次操作 \(3\), 考虑枚举第一行 \(1\) 的位置,然后同上得出列的操作序列,再判断其余每行操作/不操作后是否只有一个 \(1\) 即可。
上述操作都可以用 bitset 维护。
时间复杂度 \(\mathcal{O}(\dfrac{n^3}{\omega})\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 1e3 + 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
cout << arg << ' ';
dbg(args...);
}
int n, k;
char s[N][N];
bitset<N>f[N];
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> (s[i] + 1);
for (int j = 1; j <= n; j++) f[i][j] = (s[i][j] == 'o');
}
if (k == n) { // 可能有操作 3 前每行恰好一个 1 的情况
for (int i = 1; i <= n; i++) {
f[1].flip(i);
bool ok = 1;
for (int j = 2; j <= n && ok; j++) {
f[j] ^= f[1];
if (min(f[j].count(), n - f[j].count()) > 1) ok = 0;
f[j] ^= f[1];
}
if (ok) { cout << "DA\n"; return 0; }
f[1].flip(i);
}
}
for (int i = 1; i <= n; i++) { // 这一行不需要提前翻转,因为这一行的翻转相当于除了它的每一行都翻转
int sum = 0;
for (int j = 1; j <= n; j++) if (i != j) {
f[j] ^= f[i];
sum += min(f[j].count(), n - f[j].count());
f[j] ^= f[i];
}
if (sum <= k) { cout << "DA\n"; return 0; }
}
cout << "NE\n";
return 0;
}
AT_abc361_G
首先题意转化为给定一些黑点,其余点是白点,求不包含 \((-1, -1)\) 的白点连通块的大小之和,由于行数很少,所以我们可以维护每行的连续段,并且用并查集将两行之间的连续段合并即可。
时间复杂度 \(\mathcal{O}(n \log n + (n + V) \alpha(n + V))\), 其中 \(V\) 为行数。
点击查看代码
// LUOGU_RID: 166192829
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
typedef long long ll;
typedef pair<int, int> pii;
struct Node {
int x, l, r;
};
vector<Node> a;
vector<int> st[N];
int n, fa[N];
int findFa(int pos) { return pos == fa[pos] ? pos : fa[pos] = findFa(fa[pos]); }
void merge(int x, int y) { fa[findFa(x)] = findFa(y); }
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int x, y;
scanf("%d%d", &x, &y);
st[x].push_back(y);
}
int inf = 1 << 30;
a.push_back({-1, -inf, inf});
for (int i = 0; i <= (int) 2e5; i++) {
if (st[i].empty()) {
a.push_back({i, -inf, inf});
} else {
sort(st[i].begin(), st[i].end());
a.push_back({i, -inf, st[i][0] - 1});
for (int j = 0; j < st[i].size() - 1; j++)
if (st[i][j + 1] - st[i][j] > 1) a.push_back({i, st[i][j] + 1, st[i][j + 1] - 1});
a.push_back({i, st[i].back() + 1, inf});
}
}
int cnt = (int)a.size();
for (int i = 0; i < cnt; i++) fa[i] = i;
for (int i = 0; i < cnt; i++) if (a[i].l == -inf && a[i].r == inf) merge(i, 0);
for (int i = 0, j = 0; i < cnt && j < cnt; i++) {
while (j < cnt && (a[j].x < a[i].x + 1 || a[j].r < a[i].l)) j++;
while (j < cnt && (a[j].x == a[i].x + 1 && a[j].l <= a[i].r)) merge(i, j), j++;
j--; //与第 i 个有交的最后一个可能与第 i + 1 个有交
}
ll ans = 0;
for (int i = 1; i < cnt; i++) if (findFa(i) != findFa(0)) ans += a[i].r - a[i].l + 1;
printf("%lld\n", ans);
return 0;
}
P7562
考虑用 set 维护区间,重载运算符 \(<\) 为 \(A.r < B.l\), 这样的话 set 会把相交的区间认定为相同的(set 的逻辑:\(a = b \Leftrightarrow \lnot(a < b) \land \lnot(b < a)\)),但是这也有个弊端:如果有三个区间 \([l_1, r_1], [l_2, r_2], [l_3, r_3]\) 其中 \([l_1, r_1], [l_2, r_2]\), \([l_2, r_2], [l_3, r_3]\) 相交但是 \([l_1, r_1], [l_3, r_3]\) 不交,那 set 会认为这三个区间都是相同的,这显然是不对的,但是这是题外话,这题不会出现这种情况。
由于要求字典序最小,所以编号从小到大判断是否能填,假设当前已经填了 \(x\) 个,那么填完这个区间之后剩下的位置至少能填 \(k - x - 1\) 个,我们首先考虑一个区间最多能完成几个任务:用倍增维护即可。然后每次如果能填,把这个任务的区间从 set 里扔掉(set 一开始维护的是整个区间),离散化是显然需要的,注意任务的端点是可以相交的。
由于显然任意时刻 set 中的区间不会有交,所以重载的作用只是为了 find.
时间复杂度 \(\mathcal{O}(n \log n)\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
typedef long long ll;
typedef pair<int, int> pii;
struct Segment {
int l, r;
bool operator < (const Segment &A) const {
return r < A.l; //这样重载小于号可以让 set 认为两条除了端点相交的线段相等, 因为 a == b <=> !(a < b) && !(b < a), 这样就能自动去重
}
} a[N];
int n, k, c[N << 1], tot;
int f[20][N << 1];
set<Segment>st;
vector<int>ans;
int query(int l, int r) { //这段区间里能选多少条线段
int res = 0, now = l;
for (int d = 19; d >= 0; d--) if (f[d][now] <= r) now = f[d][now], res += (1 << d);
return res;
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &a[i].l, &a[i].r);
c[++tot] = a[i].l, c[++tot] = a[i].r;
}
sort(c + 1, c + tot + 1);
int len = unique(c + 1, c + tot + 1) - (c + 1);
for (int i = 1; i <= n; i++) {
a[i].l = lower_bound(c + 1, c + len + 1, a[i].l) - c;
a[i].r = lower_bound(c + 1, c + len + 1, a[i].r) - c;
} //离散化
memset(f, 0x3f, sizeof f);
for (int i = 1; i <= n; i++) f[0][a[i].l] = min(f[0][a[i].l], a[i].r);
for (int i = len; i >= 1; i--) f[0][i] = min(f[0][i], f[0][i + 1]); //后缀最小值
for (int d = 1; d < 20; d++)
for (int i = 1; i <= len; i++) if (f[d - 1][i] <= len) f[d][i] = f[d - 1][f[d - 1][i]];
//倍增, f[d][i] 表示从 i 开始走 1 << d 步能到的最小的右端点
st.insert({1, len}); //记录空闲的线段(没有被占用)
int las = query(1, len);
if (las < k) {
puts("-1");
return 0;
}
for (int i = 1; i <= n && ans.size() < k; i++) { //按字典序从大到小看能否放到答案里
auto it = st.find(a[i]);
if (it == st.end()) continue;
//如果找不到和 a[i] 相交的空闲线段就不能把 a[i] 放进来
// 注意 set 中区间任意时刻无交
// 能找到恰好一个包含的, 则只要判断 it 即可
if (it -> l <= a[i].l && a[i].r <= it -> r) {
//如果选了 a[i], 剩下的空间还能放下至少 (k - 目前选的线段数) 条线段, 则可以把 a[i] 加入答案
if (las - query(it -> l, it -> r) + query(it -> l, a[i].l) + query(a[i].r, it -> r) >= k - ans.size() - 1) {
las = las - query(it -> l, it -> r) + query(it -> l, a[i].l) + query(a[i].r, it -> r);
ans.push_back(i);
int u = it -> l, v = it -> r;
st.erase(it); st.insert({u, a[i].l}); st.insert({a[i].r, v});
}
}
}
for (auto x : ans) printf("%d\n", x);
return 0;
}
AT_abc221_g
直接看代码吧。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 10, M = 3.6e6 + 10, mod = 998244353;
typedef long long ll;
typedef pair<int, int> pii;
void cmax(int &x, int y) { if (x < y) x = y; }
void cmin(int &x, int y) { if (x > y) x = y; }
void add(int &x, int y) { x += y; if (x >= mod) x -= mod; }
void mul(int &x, int y) { x = 1ll * x * y % mod; }
template<typename T>
void dbg(const T &t) { cerr << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cerr << arg << ' ';
dbg(args...);
}
// 坐标轴旋转 45° 并放大 sqrt(2) 倍后, 原本坐标为 (x, y) 的点坐标变成了 (x - y, x + y), 终点变为了 (A - B, A + B), 起点还是 (0, 0)
// 那么移动也变成了 U(-d, d), D(d, -d), L(-d, -d), R(d, d)
// 即每次可以使横坐标 +d/-d 纵坐标 +d/-d, 可以分开考虑
// 对于方程 d1 ± d2 ± ... ± dn = A + B, 两边同时加 \sum d[i], 再除以 2, 得到若干 di 之和等于 (A + B + \sum d[i]) / 2, bitset 优化 01 背包即可
// 时间复杂度 O(n \times \sum d[i] / omega)
int n, a, b, d[N], sum, m1, m2;
bool op1[N], op2[N];
bitset<M>dp[N];
void solve(int sum, bool *op) {
if (!dp[n][sum]) puts("No"), exit(0);
for (int i = n; i; i--) if (sum >= d[i] && dp[i - 1][sum - d[i]]) sum -= d[i], op[i] = true;
}
map<int, char>mp[2];
int main() {
mp[0][1] = 'U', mp[1][0] = 'D', mp[0][0] = 'L', mp[1][1] = 'R';
scanf("%d%d%d", &n, &m1, &m2);
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
scanf("%d", &d[i]);
sum += d[i];
dp[i] = (dp[i - 1] | (dp[i - 1] << d[i]));
}
a = m1 - m2 + sum, b = m1 + m2 + sum;
if (a < 0 || b < 0 || a % 2 || b % 2) return puts("No"), 0;
a >>= 1, b >>= 1;
solve(a, op1), solve(b, op2);
puts("Yes");
for (int i = 1; i <= n; i++) putchar(mp[op1[i]][op2[i]]);
return 0;
}
另外这题有个更快的 \(\mathcal{O}(n \max d_i)\) 做法,收录在 https://www.cnblogs.com/Loop1st/p/18706177.
CF1889C2
考虑 dp, \(f_{i, j}\) 表示考虑前 \(i\) 个点,\(i\) 不被覆盖,操作 \(j\) 次,最多覆盖多少个数。
考虑转移,记所有包含 \(i\) 的区间 \([l, r]\), 当从 \(k\) 转移来时如果 \(l < k\) 就不需要操作,因为在 \(k\) 的时候已经操作过了,所以
其中 \(t\) 为 \(l > k\) 的 \([l, r]\) 个数。由于 \(t\) 随 \(k\) 增大而减小且为 \(\mathcal{O}(K)\) 级别,所以直接根据 \(t\) 相同的 \(k\) 分段,使用 RMQ 即可。时间复杂度 \(\mathcal{O}(nK^2r)\),其中 \(r\) 为 RMQ 复杂度。
由于这个 RMQ 的修改只有从末尾加入,所以可以直接 ST 表,但是要魔改一下,首先开 \(K\) 个 ST 表,然后 \(st_{i, j}\) 表示的是 \([j - 2^i + 1, j]\) 的区间最大值。
时间复杂度 \(\mathcal{O}(nK^2)\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10, K = 11, inf = 1 << 30;
typedef long long ll;
typedef pair<int, int> pii;
int T, n, m, k, dp[K][N];
multiset<int>s;
vector<int>a[N], t1[N], t2[N];
int st[K][20][N];
bool ok[N];
void update(int p, int i) {
st[p][0][i] = dp[p][i];
for (int j = 1; i - (1 << (j - 1)) >= 0; j++) st[p][j][i] = max(st[p][j - 1][i], st[p][j - 1][i - (1 << (j - 1))]);
}
int ask(int p, int l, int r) {
if (l > r) return -inf;
int x = __lg(r - l + 1);
return max(st[p][x][r], st[p][x][l + (1 << x) - 1]);
}
int main() {
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= n; i++) t1[i].clear(), t2[i].clear(), a[i].clear();
s.clear();
for (int i = 1; i <= m; i++) {
int l, r;
scanf("%d%d", &l, &r);
t1[l].push_back(l), t2[r + 1].push_back(l);
}
for (int i = 1; i <= n; i++) {
for (auto x : t1[i]) s.insert(x);
for (auto x : t2[i]) s.erase(s.find(x));
ok[i] = (s.size() <= k);
if (ok[i]) {
for (auto x : s) a[i].push_back(x);
a[i].push_back(i);
}
}
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (ok[i] && a[i].size() == 1) cnt++, dp[0][i] = cnt;
else if (ok[i] && a[i].size() > 1) dp[0][i] = cnt;
else dp[0][i] = 0;
}
for (int i = 0; i <= n; i++) update(0, i);
for (int j = 1; j <= k; j++) {
for (int i = 1; i <= n; i++) {
dp[j][i] = 0;
if (!ok[i]) {
update(j, i);
continue;
}
int las = 0, now = a[i].size() - 1;
for (auto x : a[i]) {
if (j < now) break;
dp[j][i] = max(dp[j][i], ask(j - now, las, x - 1) + 1);
now--, las = x;
}
update(j, i);
}
}
int ans = 0;
for (int i = 1; i <= n; i++) ans = max(ans, dp[k][i]);
printf("%d\n", ans);
}
}
P3978
假设 \(f_n, g_n\) 表示 \(n\) 个节点的二叉树个数,叶子节点总数。
下证 \(g_n = nf_{n - 1}\):
考虑到 \(n\) 个节点的二叉树都是 \(n - 1\) 个的加一个叶子节点得来。对于每一颗 \(n - 1\) 个点的二叉树,设有 \(a\) 个叶子,\(b\) 个有只有一个儿子的点,\(c\) 个有两个儿子的点,那么显然 \(a + b + c = n - 1, b + 2c = n - 2\), 而这颗树上有 \(2a + b\) 个点可以挂叶子,由上知 \(2a + b = n\), 所以 \(g_n = nf_{n - 1}\)。
又有 \(f_n\) 为卡特兰数,答案为 \(\dfrac{g_n}{f_n}=\dfrac{nf_{n - 1}}{f_n}\), 化简得 \(\dfrac{n(n+1)}{2(2n-1)}\)。
时间复杂度 \(\mathcal{O}(1)\)
P13833
前言
这篇题解没什么好的地方,只是作者看了出题人题解之后写的。
下面记 \(V = \max l_i\)。
两个重要结论
- \(f(a)\) 为 \(a\) 排序后相邻两数异或的最小值。
证明:考虑 01trie 求解的过程易证。 - \(f(a) \le \dfrac{2\max a_i}{n-1}\)。
证明:若 \(f(a) \ge 2^k\), 则 \(\lfloor\dfrac{a_i}{2^k}\rfloor\) 两两不同(如果相同会使 \(f(a)\) 二进制下 \(\ge 2^k\) 的位置全部都是 \(0\)),且这个条件是充分的。所以 \(\max a_i \ge (n - 1)2^k, 2^k \le \dfrac{\max a_i}{n-1}\), 令 \(k\) 为符合条件的最大的 \(k\),则 \(f(a) \le 2^{k + 1} \le \dfrac{2\max a_i}{n-1}\)。
这个结论的用处下面再说。
正解
令 \(g(k)\) 为 \(f(a) > k\) 的 \(a\) 个数,因为 \(f(a)\) 会在 \(g(0), g(1), \cdots, g(f(a) - 1)\) 中都算一遍,共 \(f(a)\) 遍,所以 \(\sum\limits_a f(a) = \sum\limits_{k=0}^{\max f(a)} g(k)\)。且根据结论 \(2\), \(k\) 的范围是 \(\mathcal{O}(\dfrac{V}{n})\) 的。
考虑计算 \(g(k)\)。令 \(c_i = \sum\limits_{j=1}^{n} [l_j \ge i]\)。\(a\) 没有一个好的性质,难以处理,考虑到结论 \(1\), 我们可以令一个从大到小(和从小到大是一样的,但是方便下面计数)排序的序列 \(b\), 满足 \(f(b) > 0\)(\(f(b) = 0\) 没有意义),所以 \(b\) 中元素两两不同。那么这个 \(b\) 对应了多少个 \(a\) 呢?(即多少个 \(a\) 降序排序后等于 \(b\)),我们从大到小考虑每一位,对于 \(b_i\), 它所能选的 \(a_j\) 的位置只能是 \(l_j \ge b_i\) 的 \(j\) 且没有被 \(b_1, b_2, \cdots, b_{i - 1}\) 选过,这样的 \(j\) 有 \(c_{b_i} - (i - 1)\) 个。所以总方案数为 \(\prod\limits_{i=1}^n (c_{b_i} - i + 1)\)。
考虑 dp, 设 \(f_{i, j}\) 为考虑到 \(b_1, \cdots, b_i\), 且 \(b_i = j\) 的方案数,则转移
对于每个 \(k\) 复杂度都为 \(\mathcal{O}(nV^2)\) 所以总复杂度为 \(\mathcal{O}(V^3)\)。
状态应该比较对,考虑优化转移,同时转移所有 \(j\)。
枚举 \(j \oplus x\) 从高位到低位第一个和 \(k\) 不同的位 \(h\), 且 \(k\) 这一位为 \(0\), 这样 \(j \oplus x\) 肯定大于 \(k\) 且不会算重。
将 \(j, x, k\) 同时除以 \(2^h\) 并向下取整,得到 \(j', x', k'\), 即只考虑第 \(h\) 位及以上。则 \(j' \oplus x' = (k' + 1)\)。枚举 \(j'\) 得到 \(x' = (k' + 1) \oplus j'\),由于 \(k' + 1 \neq 0\), 所以 \(j \neq x\), 就通过 \(j', x'\) 得到了 \(j, x\) 的大小关系,\(j' > x'\) 在前面已经算过了,不管。\(j, x\) 在二进制下后 \(h\) 位任意填。所以转移为将 \(f_{i, j}, j \in [j'2^h, (j' + 1)2^h)\) 加上 \(\sum\limits_{t=x'2^h}^{(x'+1)2^h-1}f_{i - 1,t}\)。使用差分和前缀和即可。
时间复杂度 \(\mathcal{O}(V^2)\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 1.7e4 + 10, mod = 998244353;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
#ifdef ONLINE_JUDGE
return ;
#endif
cout << arg << ' ';
dbg(args...);
}
int n, mx, l[N], c[N], f[N], g[N];
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> l[i], mx = max(mx, l[i]);
for (int i = 0; i <= mx; i++) for (int j = 1; j <= n; j++) c[i] += (l[j] >= i);
int up = mx * 2 / (n - 1), ans = 0;
for (int k = 0; k < up; k++) {
for (int i = 0; i <= mx; i++) f[i] = c[i];
for (int i = 2; i <= n; i++) {
for (int j = mx; j >= 0; j--) f[j] = (f[j] + f[j + 1]) % mod;
for (int h = __lg(mx); h >= 0; h--) if (!(k >> h & 1)) {
int t = k >> h | 1;
for (int j = 0; j <= (mx >> h); j++) {
int l = j << h, r = (t ^ j) << h;
if (l <= r) {
int val = (f[r] - f[r + (1 << h)] + mod) % mod;
g[l] = (g[l] + val) % mod; g[l + (1 << h)] = (g[l + (1 << h)] - val + mod) % mod;
}
}
}
for (int j = 1; j <= mx; j++) g[j] = (g[j] + g[j - 1]) % mod;
for (int j = 0; j <= mx; j++) f[j] = (ll)g[j] * (c[j] - i + 1) % mod, g[j] = 0;
}
for (int i = 0; i <= mx; i++) ans = (ans + f[i]) % mod;
}
cout << ans << '\n';
return 0;
}
P5025
下面记原本的爆炸半径为 \(t_i\)。
考虑到如果分别计算左右是简单的,双指针一下就行但是有可能往右炸的时候引爆了一个特别牛的炸弹,甚至还把更左边且原本爆不到的给引爆了,故而我们先预处理出当前炸弹一直往左能到哪里,记为 \(l_i\),然后用这些炸弹更新当前点向右炸的半径(具体地,若为 \(j\) 则用 \(t_j - (x_i - x_j)\) 更新,注意这里 \(t_j\) 也是被更新过的)。然后在用 \(t_i\) 找 \(r_i\)(右边最远的)即可。
时间复杂度 \(\mathcal{O}(n)\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 5e5 + 10, mod = 1e9 + 7;
template<typename T>
void dbg(const T &t) { cout << t << endl; }
template<typename Type, typename... Types>
void dbg(const Type& arg, const Types&... args) {
cout << arg << ' ';
dbg(args...);
}
int n, l[N], r[N];
ll a[N], t[N];
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i] >> t[i];
l[i] = r[i] = i;
}
for (int i = 2; i <= n; i++) {
while (l[i] > 1 && a[i] - a[l[i] - 1] <= t[i]) {
t[i] = max(t[i], t[l[i] - 1] - (a[i] - a[l[i] - 1]));
l[i] = l[l[i] - 1];
}
}
for (int i = n - 1; i; i--) {
while (r[i] < n && a[r[i] + 1] - a[i] <= t[i]) {
l[i] = min(l[i], l[r[i] + 1]);
r[i] = r[r[i] + 1];
}
}
ll ans = 0;
for (int i = 1; i <= n; i++) ans = (ans + (ll)i * (r[i] - l[i] + 1)) % mod;
cout << ans << '\n';
return 0;
}

浙公网安备 33010602011771号