dp递推 口胡记录
[SHOI2013] 超级跳马
:矩阵乘法,前缀和
暴力很显然,设为从跳到的方案数,那么有
发现这个东西其实是一直由前面奇偶性相同的一段转移过来的,因此考虑前缀和优化
设,那么由定义有,然后我们再考虑怎么由转移而来,不难发现,联立两式有。
然后这个前缀和显然可以矩乘转移。
[JLOI2015] 有意义的字符串
不看题解连矩乘都想不到,/kk
发现的形式和一元二次方程求根公式很像,进过构造可以发现它是方程的一个解。
移项后两边同时乘有。
发现这个已经形成了矩乘形式的递推关系,但是初始是小数,不方便进行取模。
所以考虑构造,将两个根带入后两式相加发现有,并且有都为整数。
因此可以直接矩乘计算出,再考虑减去的贡献,发现这东西 分类讨论的奇偶性判断答案是否减一即可。
注意用__int128或者龟速乘。
[ABC219H] Candles
首先有经典的关灯模型,显然每次关灯都是在已选的区间向左或向右拓展一格。那么设为已选区间为 ,时间为 ,在 的左/右端点的蜡烛长度最大值。
但是时间这一维很大,考虑优化,一种比较套路的想法是先将所有蜡烛熄灭,然后时间倒流点燃,对于每个时刻,答案会损失点燃的长度非负蜡烛数。
因此每个蜡烛的贡献为,这个式子不好维护。因为存在某些蜡烛最终贡献为0,考虑只记录有效的点。具体来说,设,其中与前文的定义相同,表示区间外有个点有效,会对答案造成贡献。然后就很好做了。
时间复杂度。
点击查看代码
#include <bits/stdc++.h> #define ll long long const int MAXN = 305; ll dp[MAXN][MAXN][MAXN][2]; void solve() { int n; std::cin >> n; std::vector <std::pair<int, int> > a(n + 1); for (int i = 0; i < n; i++) { std::cin >> a[i].first >> a[i].second; } a[n] = std::make_pair(0, 0); std::sort(a.begin(), a.end()); int beg = -1; for (int i = 0; i < a.size(); i++) { if (a[i].second == 0) { beg = i; break; } } const ll INF = 1e15; for (int i = 0 ; i <= n; i++) { for (int j = 0; j <= n; j++) { for (int k = 0; k <= n; k++) { dp[i][j][k][0] = dp[i][j][k][1] = -INF; } } } for (int i = 0; i <= n; i++) { dp[beg][beg][i][0] = dp[beg][beg][i][1] = 0; } for (int len = 2; len <= n + 1; len++) { for (int l = 0; l <= n; l++) { int r = l + len - 1; if (r > n) break; for (int k = 0; k <= n; k++) { ll x1 = -INF, x2 = -INF; x1 = std::max(dp[l + 1][r][k][0] - 1ll * (a[l + 1].first - a[l].first) * k, dp[l + 1][r][k][1] - 1ll * (a[r].first - a[l].first) * k); if (k != n) x2 = std::max(dp[l + 1][r][k + 1][0] - 1ll * (a[l + 1].first - a[l].first) * (k + 1) + a[l].second, dp[l + 1][r][k + 1][1] - 1ll * (a[r].first - a[l].first) * (k + 1) + a[l].second); dp[l][r][k][0] = std::max(x1, x2); x1 = std::max(dp[l][r - 1][k][0] - 1ll * (a[r].first - a[l].first) * k, dp[l][r - 1][k][1] - 1ll * (a[r].first - a[r - 1].first) * k); if (k != n) x2 = std::max(dp[l][r - 1][k + 1][0] - 1ll * (a[r].first - a[l].first) * (k + 1) + a[r].second, dp[l][r - 1][k + 1][1] - 1ll * (a[r].first - a[r - 1].first) * (k + 1) + a[r].second); dp[l][r][k][1] = std::max(x1, x2); } } } ll ans = -INF; for (int l = 0; l <= n; l++) { for (int r = l; r <= n; r++) { for (int k = 0; k <= n; k++) { ans = std::max(ans, std::max(dp[l][r][k][0], dp[l][r][k][1])); } } } std::cout << ans << "\n"; } int main() { std::ios::sync_with_stdio(0); std::cin.tie(0); std::cout.tie(0); int t = 1; while (t--) { solve(); } return 0; }
[AGC012E] Camel and Oases
首先由于每次跳跃时的值都会除以2,所以只有个值,不难想到固定一个的值,可以算出每个点能向左/右拓展到的最远点,分别记为。
然后发现如果我们将每个固定的形成的若干个区间的集合看成一层,那么原问题等价于对于每个在除了这一层中之后的每一层都选恰好一个区间,能否使得它们的并集为,简称能为可达
那么显然如果可达,就有从开始的前缀和从开始的后缀能够覆盖所在区间。考虑设为在状态为的层中选择了点能覆盖前缀的最大值,同理,很好转移。
对于每个, 若 与 的交覆盖了所在的区间 ,则这个区间是可行的。
复杂度
扩张
一类形如 的转移方程的优化,
[ARC101E] Ribbons on Tree
三个月前做过,现在不会了,不像某位神仙两年前做过现在一眼秒了😅
比较navie的想法是设为的子树中有个点未被覆盖,需要子树外一个点向内某个点相连的方案数,直接类似树背包转移有 。
系数过于复杂,不便于优化。看到每条边至少被覆盖一次考虑容斥。
考虑边集中的边一次都没被覆盖过,这等价于删去这些边。此时整张图被分成了若干个联通块,显然每个联通块内部匹配的方案数是 ,注意为奇数时。
数据范围无法枚举子集,考虑,重定义为以为根的子树联通块大小为的方案数(考虑容斥系数)。那么有这条边断掉 (断边集合大小加一),保留这条边 。
最后答案即为 。复杂度为树形背包的 。
点击查看代码
#include <bits/stdc++.h> void solve() { int n; std::cin >> n; std::vector <std::vector<int> > adj(n + 1); for (int i = 1; i < n; i++) { int u, v; std::cin >> u >> v; adj[u].push_back(v); adj[v].push_back(u); } const int MOD = 1e9 + 7; std::vector <int> g(n + 1); g[0] = 1; for (int i = 2; i <= n; i += 2) g[i] = 1ll * g[i - 2] * (i - 1) % MOD; std::vector <std::vector<int> > dp(n + 1); std::vector <int> siz(n + 1); for (int i = 0; i <= n; i++) dp[i].resize(n + 1); auto dfs = [&](auto self, int u, int fa) -> void { siz[u] = 1; dp[u][1] = 1; for (const auto &v : adj[u]) { if (v == fa) continue; self(self, v, u); std::vector <int> tmp(siz[u] + siz[v] + 1); for (int j = 1; j <= siz[u]; j++) { for (int k = 1; k <= siz[v]; k++) { tmp[j] += -1ll * dp[u][j] * dp[v][k] % MOD * g[k] % MOD; tmp[j] %= MOD; tmp[j] += MOD; tmp[j] %= MOD; tmp[j + k] += 1ll * dp[u][j] * dp[v][k] % MOD; tmp[j + k] %= MOD; } } siz[u] += siz[v]; for (int i = 1; i <= siz[u]; i++) dp[u][i] = tmp[i]; } }; dfs(dfs, 1, 0); int ans = 0; for (int i = 1; i <= n; i++) { ans += 1ll * dp[1][i] * g[i] % MOD; ans %= MOD; } std::cout << ans << "\n"; } int main() { std::ios::sync_with_stdio(0); std::cin.tie(0); std::cout.tie(0); int t = 1; while (t--) { solve(); } return 0; }
「GLR-R3」惊蛰
tag:优化,线段树
暴力很显然,状态为设前个,其中最后一个数为的最小答案,可以暴力 转移。
发现可以前缀最小值优化,并且第二维的取值只可能是,于是可以离散化优化到。
设离散化后数组为,离散化数组为,考虑变换状态,重定义为时最小值(即原dp的后缀和),那么转移过程为
发现这种形式可以用线段树维护。
具体来说,第一种转移直接区间加。第二种主要是区间加打标记即可。主要难在保持的单调性。
但是又发现与两个数组都是单调的,加完后,都是单调的。为了维护整个单调性,用第一部分的最小值,在线段树上二分出取的影响区间,然后区间覆盖即可。
复杂度。
点击查看代码
#include <bits/stdc++.h> const int MAXN = 1e6 + 5; template <class T> void read(T &x) { x = 0; char c = getchar(); bool f = 0; while (!isdigit(c)) f = c == '-', c = getchar(); while (isdigit(c)) x = x * 10 + c - '0', c = getchar(); x = f ? (-x) : x; } #define ll long long int lsh[MAXN]; struct Sgt { struct Node { int l, r; ll mx; ll add, tag, addb; }t[MAXN * 8]; void pushup(int p) { t[p].mx = std::max(t[p << 1].mx, t[p << 1 | 1].mx); } void pushadd(int p, ll tg) { t[p].mx += tg; t[p].add += tg; } void pushaddb(int p, ll tg) { t[p].mx += 1ll * lsh[t[p].l] * tg; t[p].addb += tg; } void pushtag(int p, ll tg) { t[p].mx = t[p].tag = tg; t[p].add = t[p].addb = 0; } void pushdown(int p) { if (t[p].tag != -1) { pushtag(p << 1, t[p].tag); pushtag(p << 1 | 1, t[p].tag); t[p].tag = -1; } if (t[p].add) { pushadd(p << 1, t[p].add); pushadd(p << 1 | 1, t[p].add); t[p].add = 0; } if (t[p].addb) { pushaddb(p << 1, t[p].addb); pushaddb(p << 1 | 1, t[p].addb); t[p].addb = 0; } } void build(int p, int l, int r) { t[p].l = l; t[p].r = r; t[p].tag = -1; if (l == r) return; int mid = (l + r) >> 1; build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r); } void add(int p, int l, int r, ll delta) { if (l <= t[p].l && t[p].r <= r) { pushadd(p, delta); return; } if (t[p].l != t[p].r) pushdown(p); int mid = (t[p].l + t[p].r) >> 1; if (l <= mid) add(p << 1, l, r, delta); if (r > mid) add(p << 1 | 1, l, r, delta); pushup(p); } void addb(int p, int l, int r, ll delta) { if (l <= t[p].l && t[p].r <= r) { pushaddb(p, delta); return; } if (t[p].l != t[p].r) pushdown(p); int mid = (t[p].l + t[p].r) >> 1; if (l <= mid) addb(p << 1, l, r, delta); if (r > mid) addb(p << 1 | 1, l, r, delta); pushup(p); } void assign(int p, int l, int r, ll v) { if (l <= t[p].l && t[p].r <= r) { pushtag(p, v); return; } if (t[p].l != t[p].r) pushdown(p); int mid = (t[p].l + t[p].r) >> 1; if (l <= mid) assign(p << 1, l, r, v); if (r > mid) assign(p << 1 | 1, l, r, v); pushup(p); } ll query(int p, int pos) { if (t[p].l == t[p].r) { return t[p].mx; } if (t[p].l != t[p].r) pushdown(p); int mid = (t[p].l + t[p].r) >> 1; if (pos <= mid) return query(p << 1, pos); return query(p << 1 | 1, pos); } int binary(int p, ll x) { if (t[p].l != t[p].r) pushdown(p); if (t[p].mx < x) return -1; else if (t[p].l == t[p].r) return t[p].l; int mid = (t[p].l + t[p].r) >> 1; int ret = binary(p << 1 | 1, x); if (ret != -1) return ret; return binary(p << 1, x); } //last position i where dp[i] >= x }; void solve() { int n, c; std::cin >> n >> c; std::vector <int> a(n + 1); for (int i = 1; i <= n; i++) { read(a[i]); lsh[i] = a[i]; } std::sort(lsh + 1, lsh + 1 + n); int tot = std::unique(lsh + 1, lsh + 1 + n) - lsh - 1; for (int i = 1; i <= n; i++) { a[i] = std::lower_bound(lsh + 1, lsh + 1 + tot, a[i]) - lsh; a[i] = tot - a[i] + 1; // std::cerr << a[i] << " "; } // std::cerr << "\n"; std::reverse(lsh + 1, lsh + 1 + tot); static Sgt t; t.build(1, 1, tot); for (int i = 1; i <= n; i++) { if (a[i] + 1 <= tot) t.add(1, a[i] + 1, tot, c); t.add(1, 1, a[i], -lsh[a[i]]); t.addb(1, 1, a[i], 1); ll val = t.query(1, a[i]); // std::cerr << "qwq" << "\n"; int pos = t.binary(1, val); // int pos = tot; // std::cerr << "qaq" << "\n"; if (pos != -1 && a[i] + 1 <= pos) t.assign(1, a[i] + 1, pos, val); } ll ans = 1e18; for (int i = 1; i <= tot; i++) ans = std::min(ans, t.query(1, i)); std::cout << ans << "\n"; } int main() { // freopen("wave.in", "r", stdin); // freopen("wave.out", "w", stdout); // std::ios::sync_with_stdio(0); // std::cin.tie(0); // std::cout.tie(0); int t = 1; while (t--) { solve(); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】