dp递推 口胡记录

[SHOI2013] 超级跳马

tag:矩阵乘法,前缀和

暴力dp很显然,设fi,j为从(1,1)跳到(i,j)的方案数,那么有fi,j=j(2k+1)>0fi/i+1/i1,j(2k+1)

发现这个东西其实是一直由前面奇偶性相同的一段转移过来的,因此考虑前缀和优化

si,j=j2k>0fi,j2k,那么由定义有si,j=si,j2+fi,j,然后我们再考虑f怎么由s转移而来,不难发现fi,j=si1,j1+si,j1+si+1,j1,联立两式有si,j=si,j2+si,j1+si1,j+si+1,j1

然后这个前缀和显然可以矩乘转移。

[JLOI2015] 有意义的字符串

不看题解连矩乘都想不到,/kk

发现b+d2的形式和一元二次方程求根公式很像,进过构造可以发现它是方程x2bx+b2d4=0的一个解。

移项后两边同时乘xn2xn=bxn1b2d4xn2

发现这个已经形成了矩乘形式的递推关系,但是初始b+d2是小数,不方便进行取模。

所以考虑构造f(x)=(b+d2)x+(bd2)x,将两个根x1,x2带入后两式相加发现有f(i)=bf(i1)b2d4f(i2),并且有f(0),f(1),b,b2d4都为整数。

因此可以直接矩乘计算出f(n),再考虑减去(bd2)n的贡献,发现这东西 {1,1}分类讨论n的奇偶性判断答案是否减一即可。

注意用__int128或者龟速乘。

[ABC219H] Candles

首先有经典的关灯模型,显然每次关灯都是在已选的区间向左或向右拓展一格。那么设dpl,r,t,0/1为已选区间为[l,r] ,时间为 t ,在 [l,r] 的左/右端点的蜡烛长度最大值。

但是时间这一维很大,考虑优化,一种比较套路的想法是先将所有蜡烛熄灭,然后时间倒流点燃,对于每个时刻,答案会损失点燃的长度非负蜡烛数。

因此每个蜡烛的贡献为max(0,ait),这个式子不好维护。因为存在某些蜡烛最终贡献为0,考虑只记录有效的点。具体来说,设dpl,r,k,0/1,其中l,r,0/1与前文的定义相同,k表示[l,r]区间外有k个点有效,会对答案造成贡献。然后就很好做了。

时间复杂度O(n3)

点击查看代码
#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

首先由于每次跳跃时v的值都会除以2,所以只有O(logn)v值,不难想到固定一个v的值,可以算出每个点能向左/右拓展到的最远点,分别记为li,ri

然后发现如果我们将每个固定的v形成的若干个区间的集合看成一层,那么原问题等价于对于每个i在除了v=v这一层中之后的每一层都选恰好一个区间,能否使得它们的并集为[1,n],简称能为可达

那么显然如果i可达[1,n],就有从1开始的前缀和从n开始的后缀能够覆盖i所在区间。考虑设dplmask为在状态为mask的层中选择了点能覆盖前缀的最大值,dprmask同理,很好转移。

对于每个i, 若dplmaskdprmask2 的交覆盖了i所在的区间 (mask1|mask2=all),则这个区间是可行的。

复杂度O(nlogv)

扩张

一类形如 dpmask=(|mask||sub|)×w(mask)的转移方程的优化,O(3n)>O(2n×n2)

image

[ARC101E] Ribbons on Tree

三个月前做过,现在不会了,不像某位神仙两年前做过现在一眼秒了😅

比较navie的想法是设dpu,xu的子树中有x个点未被覆盖,需要子树外一个点向内某个点相连的方案数,直接类似树背包转移有 dpu,x+y2k=vson(u),x+y>0,kmin(x,y)dpu,x×dpv,y×(xk)×(yk)×k!

系数过于复杂,不便于优化。看到每条边至少被覆盖一次考虑容斥。

考虑边集S中的边一次都没被覆盖过,这等价于删去这些边。此时整张图被分成了若干个联通块,显然每个联通块内部匹配的方案数是 g(n)=2kn,kN2k1,注意n为奇数时g(n)=0

数据范围无法枚举子集,考虑dp,重定义dpu,x为以u为根的子树联通块大小为x的方案数(考虑容斥系数)。那么有这条边断掉 dpu,x×dpv,y×g(y)×1dpu,x (断边集合大小加一),保留这条边 dpu,x×dpv,ydpu,x+y

最后答案即为 i=1ndp1,i×g(i)。复杂度为树形背包的 O(n2)

点击查看代码
#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优化,线段树

暴力dp很显然,状态为设dpi,jib,其中最后一个数为j的最小答案,可以暴力 O(nV2)转移。

发现可以前缀最小值优化,并且第二维的取值只可能是ai,于是可以离散化优化到O(n2)

设离散化后数组为a,离散化数组为lsh,考虑变换dp状态,重定义dpi,jlshjbi时最小值(即原dp的后缀和),那么dp转移过程为image
image
发现这种形式可以用线段树维护。

具体来说,第一种转移直接区间加。第二种主要是区间加lshj打标记即可。主要难在保持dpi的单调性。

但是又发现lshdpi两个数组都是单调的,加完后dpi,1dpi,kdpi,k+1dpi,n都是单调的。为了维护整个dpi单调性,用第一部分的最小值,在线段树上二分出取min的影响区间,然后区间覆盖即可。

复杂度O(nlogn)

点击查看代码
#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;
}
posted @   cqbzlzh  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示