动态规划优化 习题总结 1
动态规划优化 习题总结
《算法竞赛进阶指南》中的例题与练习题。
*A. P1081 [NOIP2012 提高组] 开车旅行(倍增优化 DP)
洛谷 P1081 | AcWing 293 | 码创未来
题意
小 A 和小 B 开车去东西排列的连续 \(n\) 座城市旅行,城市自西向东编号依次为 \(1\) 到 \(n\),已知各个城市的海拔高度互不相同,记城市 \(i\) 的海拔高度为 \(h_i\),则城市 \(i\) 和城市 \(j\) 之间的距离 \(d_{i,j}=|h_i-h_j|\)。
旅行过程中,小 A 和小 B 轮流开车,第一天小 A 开车,之后每天轮换一次。他们计划选择一个城市 \(s\) 作为起点,一直向东行驶,并且最多行驶 \(x\) 公里就结束旅行。
小 A 和小 B 的驾驶风格不同,小 B 总是沿着前进方向选择一个最近的城市作为目的地,而小 A 总是沿着前进方向选择第二近的城市作为目的地(注意:本题中如果当前城市到两个城市的距离相同,则认为离海拔低的那个城市更近)。如果其中任何一人无法按照自己的原则选择目的城市,或者到达目的地会使行驶的总距离超出 \(x\) 公里,他们就会结束旅行。
在启程之前,小 A 想知道两个问题:
-
对于一个给定的 \(x=x_0\),从哪一个城市出发,小 A 开车行驶的路程总数与小 B 行驶的路程总数的比值最小(如果小 B 的行驶路程为 \(0\),此时的比值可视为无穷大,且两个无穷大视为相等)。如果从多个城市出发,小 A 开车行驶的路程总数与小 B 行驶的路程总数的比值都最小,则输出海拔最高的那个城市。
-
对任意给定的 \(x=x_i\) 和出发城市 \(s_i\),小 A 开车行驶的路程总数以及小 B 行驶的路程总数。
对于 \(100\%\) 的数据:\(1\le n,m \le 10^5\),\(-10^9 \le h_i≤10^9\),\(1 \le s_i \le n\),\(0 \le x_i \le 10^9\) 。
数据保证 \(h_i\) 互不相同。
思路
本题有三个关键信息:所在城市、行驶天数、A 和 B 行驶的总距离。
由出发城市和天数我们可以知道现在所在的城市,以及当前谁在开车,并且可以算出两人分别和总共行驶的距离。
天数是一个连续的阶段,我们可以用倍增来优化。还需要设一维状态表示谁先开车,因为中间过程中不一定是 A 先开车。
-
设 \(f(i,j,k)\) 表示从城市 \(j\) 出发,一共行驶了 \(2^i\) 天,出发时 \(k\) 先开车,最终能到达的城市,其中 \(k\in\{0,1\}\),\(k=0\) 表示 A 先开车,\(k=1\) 表示 B 先开车。
对于初值 \(f(0,j,k)\),我们可以暴力处理出来。
设 \(ga(j)\) 和 \(gb(j)\) 分别表示 A 和 B 从 \(j\) 出发到达的下一个城市,\(i\in[j+1,n]\),那么 \(gb(j)\) 即是令 \(d_{i,j}\) 取得最小值的 \(i\),\(ga(j)\) 即是令 \(d_{i,j}\) 取得次小值的的 \(i\)。(注意 A 对应次小值,B 对应最小值。。。)
我们从后往前遍历城市 \(j\),将 \(h_j\) 插入一个
std::multiset即平衡树,这样已经在平衡树中的城市范围为 \([j,n]\),那么从当前城市 \(j\) 向前遍历两个、向后遍历两个城市并比较距离即可得出最近城市和第二近城市。具体实现可以参考代码。于是 \(f(0,j,0)=ga(j),f(0,j,1)=gb(j)\)。根据倍增的思想,并且特殊考虑 \(i=1\) 的情况(一共 \(2\) 天,第一天 \(k\) 开车,第二天 \(k\operatorname{xor}1\) 开车),得出状态转移方程。
\[f(i,j,k)= \begin{cases} f(i-1,f(i-1,j,k),k\operatorname{xor}1)&k=1\\ f(i-1,f(i-1,j,k),k)&k\ne1 \end{cases}\qquad i\ge1,1\le j\le n,k\in\{0,1\}. \] -
接下来处理 A 和 B 行驶的距离。我们设 \(da(i,j,k)\) 和 \(db(i,j,k)\) 分别表示从城市 \(j\) 出发,一共行驶了 \(2^i\) 天,出发时 \(k\) 先开车,A 和 B 行驶的总距离。
初值 \(da(0,j,0)=d_{j,ga(j)},da(0,j,1)=0\);\(db(0,j,0)=0,db(0,j,1)=d_{j,gb(j)}\)。
同样根据倍增思想,利用之前处理出的 \(f\) 数组,写出状态转移方程。
\[\begin{gathered} da(i,j,k)= \begin{cases} da(i-1,j,k)+da(i-1,f(i-1,j,k),k\operatorname{xor}1)&k=1\\ da(i-1,j,k)+da(i-1,f(i-1,j,k),k)&k\ne1 \end{cases} \\ db(i,j,k)= \begin{cases} db(i-1,j,k)+db(i-1,f(i-1,j,k),k\operatorname{xor}1)&k=1\\ db(i-1,j,k)+db(i-1,f(i-1,j,k),k)&k\ne1 \end{cases} \\ \qquad i\ge1,1\le j\le n,k\in\{0,1\}. \end{gathered} \] -
预处理出天数的二进制每一位的状态之后,我们来考虑一般性的问题。
设 \(calc(s,x)\) 表示从城市 \(s\) 出发,最多行驶 \(x\) 公里,A 和 B 行驶的路程。显然这个函数是一个二元组。
根据二进制拆分的思想,我们递减枚举 \(2\) 的 \(i\) 次幂,累加到行驶的天数里。
具体来说,我们设所在城市为 \(p\),A 和 B 行驶的路程分别为 \(la\) 和 \(lb\)。开始时 \(p=s,la=lb=0\)。然后倒序循环枚举 \(2\) 的幂次 \(i\),\(\log_2(n)\ge i\ge0\)。如果 \(la+lb+da(i,p,0)+db(i,p,0)\le x\),说明可以继续行驶 \(2^i\) 天,于是我们更新数值,令 \(la\gets la+da(i,p,0),lb\gets lb+db(i,p,0),p\gets f(i,p,0)\)。
循环结束后,所得的 \(la,lb\) 即为答案。
-
有了 \(calc(s,x)\) 函数,我们就可以解决题目中提出的问题了。
- 枚举 \(s\),打擂台求出 \(\arg\min\{calc(s,x_0)\}\);
- 多次询问 \(calc(s_i,x_i)\)。
代码
点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <set>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
#define int long long
using namespace std;
typedef pair<int, int> pii;
const int N = 1e5 + 10;
const int INF = 0x3f3f3f3f3f3f3f3f;
int n, m, l, ga, gb, f[20][N][2], da[20][N][2], db[20][N][2];
struct City {
int id, h;
City() {}
City(int _id, int _h): id(_id), h(_h) {}
bool operator<(City const &o) const {
return h < o.h;
}
} a[N], tmp[4];
multiset<City> myset;
inline bool cmp(City const &p, City const &q) {
return p.h == q.h ? a[p.id].h < a[q.id].h : p.h < q.h;
}
pii calc(int s, int x) {
int p = s;
int la = 0, lb = 0;
g(i, l, 0) {
if (f[i][p][0] && la + lb + da[i][p][0] + db[i][p][0] <= x) {
la += da[i][p][0];
lb += db[i][p][0];
p = f[i][p][0];
}
}
return (pii){la, lb};
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
l = log2(n);
f(i, 1, n) cin >> a[i].h, a[i].id = i;
myset.insert(City(0, INF)), myset.insert(City(0, INF));
myset.insert(City(n + 1, -INF)), myset.insert(City(n + 1, -INF));
g(i, n, 1) {
auto it = myset.insert(a[i]); //insert()函数返回值正是刚刚插入的值的迭代器(这样就不用find()函数了)
auto it1 = ++it, it2 = ++it, it3 = --(--(--it)), it4 = --it; //移动std::multiset<>::iterator只能用++和--
tmp[0] = City((*it1).id, (*it1).h - a[i].h), tmp[1] = City((*it2).id, (*it2).h - a[i].h); //*iterator表示引用迭代器所
tmp[2] = City((*it3).id, a[i].h - (*it3).h), tmp[3] = City((*it4).id, a[i].h - (*it4).h); //指向的元素, 也可以用it->h
sort(tmp, tmp + 4, cmp);
gb = tmp[0].id, ga = tmp[1].id;
f[0][i][0] = ga, f[0][i][1] = gb;
da[0][i][0] = abs(a[i].h - a[ga].h), db[0][i][1] = abs(a[i].h - a[gb].h);
}
f(j, 1, n) f(k, 0, 1) { //i=1
f[1][j][k] = f[0][f[0][j][k]][k^1];
da[1][j][k] = da[0][j][k] + da[0][f[0][j][k]][k^1];
db[1][j][k] = db[0][j][k] + db[0][f[0][j][k]][k^1];
}
f(i, 2, l) f(j, 1, n) f(k, 0, 1) { //i>1
f[i][j][k] = f[i-1][f[i-1][j][k]][k];
da[i][j][k] = da[i-1][j][k] + da[i-1][f[i-1][j][k]][k];
db[i][j][k] = db[i-1][j][k] + db[i-1][f[i-1][j][k]][k];
}
int s = 0, x;
cin >> x;
double minn = INF;
f(i, 1, n) {
pii tmp = calc(i, x);
double sum = tmp.second == 0 ? INF : ((double)tmp.first / (double)tmp.second);
if (sum < minn) minn = sum, s = i;
else if (sum == minn && a[i].h > a[s].h) s = i;
}
cout << s << '\n';
cin >> m;
f(i, 1, m) {
cin >> s >> x;
pii tmp = calc(s, x);
cout << tmp.first << ' ' << tmp.second << '\n';
}
return 0;
}
*B. Count The Repetitions(倍增优化 DP)
题意
定义 \(conn(s,n)\) 为 \(n\) 个字符串 \(s\) 首尾相接形成的字符串,例如 \(conn(\texttt{abc},2)=\texttt{abcabc}\)。
称字符串 \(a\) 能由字符串 \(b\) 生成,当且仅当从字符串 \(b\) 中删除某些字符后可以得到字符串 \(a\)。
例如 \(\texttt{abdbec}\) 可以生成 \(\texttt{abc}\),但是 \(\texttt{acbbe}\) 不能生成 \(\texttt{abc}\)。
给定两个字符串 \(s_1\) 和 \(s_2\),以及两个整数 \(n_1\) 和 \(n_2\),求一个最大的整数 \(m\),满足 \(conn(conn(s_2,n_2),m)\) 能由 \(conn(s_1,n_1)\) 生成。
\(|s_1|,|s_2|\le100\),\(n_1,n_2\le10^6\)。其中 \(|str|\) 表示字符串 \(str\) 的长度。
(注意先输入 \(s_2,n_2\) 再输入 \(s_1,n_1\)。。。)
思路
首先发现 \(conn(conn(s_2,n_2),m)=conn(s_2,n_2\times m)\)。
所以我们求的其实是最大的 \(m'=n_2\times m\),满足 \(conn(s_2,m')\) 是 \(conn(s_1,n_1)\) 的一个子序列(不一定连续)。
注意到 \(m'\) 的上界很大,为 \(\dfrac{|s_1|\times n_1}{|s_2|}\),于是我们考虑二进制拆分的思想。设 \(l=\log_2\left(\dfrac{|s_1|\times n_1}{|s_2|}\right)\),表示 \(m'\) 的二进制最多有多少位。
令 \(m'=2^{p_1}+2^{p_2}+\dots+2^{p_k}\),把 \(conn(s_2,m')\) 看做由 \(conn(s_2,2^{p_1}),conn(s_2,2^{p_2}),\dots,conn(s_2,2^{p_k})\) 拼接而成,其中 \(p_i\le l\)。
最后统计答案的时候,用尽量大的 \(2^{p_i}\) 在所用若干个 \(s_1\) 总长度不超过 \(|s_1|\times n_1\) 的情况下拼成 \(m'\)。
考虑每一个 \(conn(s_2,2^j)\)。由于 \(conn(s_1,n_1)\) 是由很多个 \(s_1\) 重复拼接而成的,我们不妨设想一个字符串 \(s_1'=conn(s_1,+\infty)\),即令 \(s_1\) 重复无数次。
由于所用的 \(s_1'\) 长度与开始匹配的起点 \(i\) 和 \(s_2\) 的重复次数 \(j\) 有关,我们设 \(f(i,j)\) 表示从 \(s_1'[i]\) 开始,要想生成 \(conn(s_2,2^j)\),需要用 \(s_1'\) 的最小连续长度。
即用 \(s_1'[i..i+f(i,j)-1]\) 能够生成 \(conn(s_2,2^j)\) 的最小 \(f(i,j)\)。
在 \(s_1'\) 中,从第 \(i\) 位开始和从第 \(i+k|s_1|\) 位开始是等效的,所以 \(f(i,j)\) 中 \(i\in[0,|s_1|)\)。
考虑倍增的思想,我们先用尽量短的 \(s_1'\) 生成 \(conn(s_2,2^{j-1})\),再从当前位置开始用尽量短的 \(s_1'\) 生成另一个 \(conn(s_2,2^{j-1})\)。
初值 \(f(i,0)\) 可以暴力匹配 \(s_1'\) 和 \(s_2\) 处理。
接下来对 DP 的阶段 \(j\) 进行拼接。
第一步,枚举起始位置 \(i\in[0,|s_1|)\);
第二步,用 \(p\) 记录当前位置,开始时 \(p=i\),从 \(l\) 到 \(0\) 倒序循环枚举 \(j\),如果当前的 \(conn(s_1,n_1)\) 足够生成 \(conn(s_2,2^j)\),即 \(p+f(p\bmod|s_1|,j)\le|s_1|\times n_1\),那么累加答案,并且改变当前位置,即令 \(p\gets p+f(p\bmod|s_1|,j)\),直到无法再生成;
第三步,打擂台更新答案,枚举下一个 \(i\)。
最后别忘了将答案除以 \(n_2\) 以得到真正的 \(m\)。
代码
点击查看代码
#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;
const int N = 110;
int n1, n2, l1, l2, l;
long long f[N][33], ans, tmp, x;
string s1, s2;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
while (cin >> s2 >> n2 >> s1 >> n1) {
l1 = s1.length(), l2 = s2.length();
l = log2(l1 * n1 / l2);
for (int i = 0; i < l1; ++i) {
int p = i;
f[i][0] = 0;
for (int j = 0; j < l2; ++j) { //匹配s2的每一位
int cnt = 0;
while (s1[p] != s2[j]) {
if (++p == l1) p = 0;
if (++cnt >= l1) { //s1中匹配不上这一位
cout << 0 << '\n';
goto label; //goto语句, 直接跳到循环最后的continue
}
}
if (++p == l1) p = 0;
f[i][0] += cnt + 1;
}
}
for (int j = 1; j <= l; ++j)
for (int i = 0; i < l1; ++i)
f[i][j] = f[i][j - 1] + f[(i + f[i][j - 1]) % l1][j - 1];
x = tmp = 0;
for (int j = l; j >= 0; --j)
if (x + f[x % l1][j] <= l1 * n1)
x += f[x % l1][j], tmp += (1 << j);
cout << tmp / n2 << '\n';
label:
continue;
}
return 0;
}
C. [USACO04DEC]Cleaning Shifts S(线段树优化 DP,离散化)
POJ 2376 | AcWing 295 | 码创未来
题意
给定 \(n\) 个区间,用 \(l_i,r_i(1\le i\le n)\) 表示,有一排 \(T\) 个格子,用区间(可以重叠)覆盖所有格子,求最小区间数。
\(1\le n\le25000,1\le T\le10^6\)。
思路
设 \(f(i)\) 表示覆盖 \([1,i]\) 最少需要多少个区间。初值 \(f(0)=0,f(i)=+\infty(i\ne0)\)。
将区间按 \(r\) 从小到大排序,枚举区间。
对于当前区间 \(i\),它能覆盖的范围为 \([l_i,r_i]\),所以需要之前覆盖的范围最少为 \([1,l_i-1]\),最多为 \([1,r_i-1]\),于是可以得出状态转移方程。
对于取 min 的操作,我们用线段树来维护。数据范围较大,需要离散化(或许也可以不离散化)。
代码
点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
const int N = 2.5e4 + 10;
const int INF = 0x3f3f3f3f;
int n, m, f[N << 2], ans = INF, raw[N << 2], cnt;
struct Node {
int l, r;
friend bool operator<(Node const &p, Node const &q) {
return p.r < q.r;
}
} a[N];
struct SegTree {
#define lson (u << 1)
#define rson (u << 1 | 1)
struct Node {
int l, r, minn;
} tr[N << 4];
inline void pushup(int u) { tr[u].minn = min(tr[lson].minn, tr[rson].minn); }
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
tr[u].minn = l ? INF : 0;
if (l == r) return;
int mid = (l + r) >> 1;
build(lson, l, mid);
build(rson, mid + 1, r);
// pushup(u);
}
void modify(int u, int x, int v) {
if (tr[u].l == x && tr[u].r == x) {
tr[u].minn = v;
return;
}
int mid = (tr[u].l + tr[u].r) >> 1;
if (x <= mid) modify(lson, x, v);
else modify(rson, x, v);
pushup(u);
return;
}
int query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) return tr[u].minn;
int res = INF;
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) res = min(res, query(lson, l, r));
if (r > mid) res = min(res, query(rson, l, r));
return res;
}
} t;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
raw[++cnt] = 1, raw[++cnt] = m;
f(i, 1, n) {
cin >> a[i].l >> a[i].r;
raw[++cnt] = a[i].l, raw[++cnt] = a[i].r;
raw[++cnt] = a[i].l + 1, raw[++cnt] = a[i].r + 1;
}
sort(a + 1, a + n + 1);
sort(raw + 1, raw + cnt + 1);
cnt = unique(raw + 1, raw + cnt + 1) - raw - 1;
while (raw[cnt] > m) --cnt;
memset(f, 0x3f, sizeof f);
f[0] = 0;
t.build(1, 0, cnt);
f(i, 1, n) {
a[i].l = lower_bound(raw + 1, raw + cnt + 1, a[i].l) - raw;
a[i].r = lower_bound(raw + 1, raw + cnt + 1, a[i].r) - raw;
int tmp = t.query(1, a[i].l - 1, a[i].r - 1) + 1;
if (f[a[i].r] > tmp) {
f[a[i].r] = tmp;
t.modify(1, a[i].r, f[a[i].r]);
}
}
if (f[cnt] == INF) cout << "-1\n";
else cout << f[cnt] << '\n';
return 0;
}
D. [USACO05DEC]Cleaning Shifts S(线段树优化 DP)
洛谷 P4644 | POJ 3171 | AcWing 296 | 码创未来
题意
给定 \(n\) 个区间,用 \(l_i,r_i(1\le i\le n)\) 表示,第 \(i\) 个区间有一个代价 \(c_i\),有一排 \(T\) 个格子,用这些区间(可以重叠)覆盖 \([L,R]\) 内的所有格子,求所需的最小代价。
\(1\le n\le10000,0\le L,R\le86399,L\le l_i\le r_i\le R,0\le c_i\le500000\)。
思路
与上一题不同之处在于不需要离散化;每个区间有一定的代价;覆盖的范围为 \([L,R]\)。
令 \(f(i)\) 表示覆盖 \([L,i]\) 所需的最小代价。初值 \(f(L-1)=0,f(i)=+\infty(L\le i\le R)\)。
用线段树维护区间最小值。答案为 \(f(R)\)。
代码
点击查看代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define il inline
using namespace std;
const int N = 1e4 + 10, M = 1e5 + 10;
int n, L, R;
long long f[M];
struct Cow {
int a, b, c;
bool operator<(Cow const &o) const {
return b < o.b;
}
} a[N];
namespace SegTree {
#define lson (u << 1)
#define rson (u << 1 | 1)
struct Node {
int l, r;
long long x;
} tr[M << 2];
il void pushup(int u) { tr[u].x = min(tr[lson].x, tr[rson].x); }
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) return tr[u].x = f[l], void();
int mid = (l + r) >> 1;
build(lson, l, mid);
build(rson, mid + 1, r);
pushup(u);
return;
}
void modify(int u, int x, long long y) {
if (tr[u].l == x && tr[u].r == x) {
tr[u].x = y;
return;
}
// pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (x <= mid) modify(lson, x, y);
else modify(rson, x, y);
pushup(u);
return;
}
long long query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].x;
}
long long res = 4557430888798830399LL; //0x3f3f3f3f3f3f3f3f
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) res = min(res, query(lson, l, r));
if (r > mid) res = min(res, query(rson, l, r));
return res;
}
}
using SegTree::build;
using SegTree::modify;
using SegTree::query;
signed main() {
scanf("%d%d%d", &n, &L, &R);
++L, ++R;
memset(f, 0x3f, sizeof f);
f[L - 1] = 0;
f(i, 1, n) {
scanf("%lld%lld%lld", &a[i].a, &a[i].b, &a[i].c);
a[i].a = max(++a[i].a, L);
a[i].b = min(++a[i].b, R);
}
sort(a + 1, a + n + 1);
build(1, L - 1, R);
f(i, 1, n) {
f[a[i].b] = min(f[a[i].b], query(1, a[i].a - 1, a[i].b - 1) + a[i].c);
modify(1, a[i].b, f[a[i].b]);
}
if (f[R] >= 4557430888798830399LL) puts("-1");
else printf("%lld\n", f[R]);
return 0;
}
*E. The Battle of Chibi(树状数组优化 DP,离散化)
洛谷 UVA12983 | AcWing 297 | 码创未来
题意
给定长度为 \(n\) 的数列 \(A\),求 \(A\) 有多少个长度为 \(m\) 的严格递增子序列。\(T\) 组测试数据。
对于每组测试数据,\(1\le m\le n\le1000,|A_i|\le10^9(1\le i\le n)\)。
对于每个测试点,数据满足 \(1\le T\le100,\sum_{i=1}^Tn_i\times m_i\le10^7\)。
思路
设 \(f(i,j)\) 表示在前 \(j\) 个数中,有多少个长度为 \(i\) 且以 \(A_j\) 为结尾的严格递增子序列。显然若 \(i>j\) 则 \(f(i,j)=0\)。初值 \(f(0,0)=1\)。
可以写出状态转移方程:
如果每次都要重新扫一遍 \(k\),那么代价是 \(O(n^3)\) 的(\(m,n\) 视为同阶),显然会喜提一个大大的 TLE。如何优化呢?
我们先把外层循环的 \(i\) 看做定值。对于每一个 \(j\),我们想知道的是满足 \(a_k<a_j\) 的 \(f(i-1,k)\) 的和。
由于 \(j\) 从小到大枚举,所以 \(k<j\) 的条件自然就满足了。我们把 \(a_k\) 当做比较大小的关键码,将 \(f(i-1,k)\) 加入树状数组,每次询问所有小于 \(a_j\) 的 \(a_k\) 的和。
由于 \(a_i\) 很大,所以需要进行离散化。我们把数组映射到区间 \([2,n+1]\) 上,把 \(-\infty\) 映射到 \(1\) 上。
当枚举到每一个 \(i\) 时,我们清空树状数组,然后向树状数组的位置 \(1\)(对应 \(-\infty\))上插入 \(f(i-1,0)\) 即 \(1(i=1)\) 或 \(0(i\ne1)\)。
然后枚举 \(j\),需要进行两个操作(顺序任意):
- 查询小于 \(a_j\) 的所有 \(a_k\) 的 \(f(i-1,k)\) 的和,修改 \(f(i,j)\) 的值;
- 向树状数组中 \(j\) 对应的位置 \(val_j\) 上插入 \(f(i-1,j)\)。
答案为 \(\sum\limits_{i=m}^nf(m,i)\)。
代码
点击查看代码
#include <iostream>
#include <cstring>
#include <unordered_map>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define il inline
using namespace std;
const int N = 1e3 + 10;
const int MOD = 1e9 + 7;
const int INF = 0x3f3f3f3f;
int tt, n, m, a[N], f[N][N], raw[N], c[N], cnt, ans;
unordered_map<int, int> val;
il int lowbit(int x) { return x & (-x); }
void add(int x, int y) {
while (x <= cnt) {
c[x] += y;
c[x] %= MOD;
x += lowbit(x);
}
return;
}
int query(int x) {
int res = 0;
while (x >= 1) {
res += c[x];
res %= MOD;
x -= lowbit(x);
}
return res;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> tt;
f(x, 1, tt) {
val.clear();
ans = 0;
cin >> n >> m;
f(i, 1, n) cin >> a[i], raw[i] = a[i];
raw[0] = -INF;
sort(raw, raw + n + 1);
cnt = unique(raw, raw + n + 1) - raw;
f(i, 0, cnt - 1) val[raw[i]] = i + 1;
// f[0][0] = 1;
f(i, 1, m) {
memset(c, 0, sizeof c);
add(1, (bool)(i == 1));
f(j, 1, n) {
f[i][j] = query(val[a[j]] - 1);
add(val[a[j]], f[i - 1][j]);
}
}
f(i, m, n) ans += f[m][i], ans %= MOD;
cout << "Case #" << x << ": " << ans << '\n';
}
return 0;
}
*F. Cut The Sequence(单调队列优化 DP)
POJ 3017 | AcWing 299 | 码创未来
TO DO...
G. Bribing FIPA(树上背包 DP)
洛谷 UVA1222 | AcWing 324 | 码创未来
题意
给定一个 \(n\) 个节点的森林和非负整数 \(m(0\le m\le n)\),节点 \(i\) 有费用 \(a_i\)。每个节点的价值即为以它为根的子树大小。
要求选定若干节点,使得价值总和 \(\ge m\)。并且如果选定了一个子树,则这个子树中的点不能再被选定。
求选定节点的费用和的最小值。
\(1\le n\le200,0\le m\le n\)。
思路
选定一个节点,就相当于选定了它下面的所有点。
设 \(f(u,i)\) 表示在以 \(u\) 为根的子树中,选定若干节点使价值总和 \(\ge i\) 的最小费用和。
令 \(v\) 是 \(u\) 的儿子,那么状态转移方程为:
初值 \(f(u,i)=+\infty,f(u,0)=0\)。
由于一个 \(f(u,i)\) 只能由一个 \(v\) 更新,所以要倒序枚举 \(i\)。这一点与背包问题很相似。
为了把森林变成一棵树,我们建立一个超级根节点 \(0\)。令 \(a_0=+\infty\)。答案为 \(\min\limits_{m\le i\le n}\{f(0,i)\}\)。
代码
点击查看代码
#include <cstdio>
#include <sstream>
#include <cstring>
#include <unordered_map>
#include <string>
#include <vector>
#include <bitset>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
using namespace std;
const int N = 210;
const int INF = 0x3f3f3f3f;
int n, m, c, a[N], siz[N], f[N][N], ans;
char s[22000];
unordered_map<string, int> mp;
vector<int> e[N];
bitset<N> isRoot;
int dfs(int u) {
memset(f[u], 0x3f, sizeof f[u]);
f[u][0] = 0;
int siz = 1;
for (auto v: e[u]) {
siz += dfs(v);
g(i, n, 0) f(j, 0, i)
f[u][i] = min(f[u][i], f[u][j] + f[v][i - j]);
}
f[u][siz] = min(f[u][siz], a[u]);
return siz;
}
signed main() {
while (true) {
fgets(s, sizeof s, stdin); //读入整行存到字符串中
if (s[0] == '#') break;
sscanf(s, "%d%d", &n, &m); //从字符串中读入数据
c = 0;
mp.clear();
f(i, 0, n) e[i].clear();
isRoot.set(); //全部设为1
f(i, 1, n) {
int x, y, tmp;
scanf("%s %d", s, &tmp);
if (mp.find(s) == mp.end()) mp[s] = ++c;
x = mp[s];
a[x] = tmp;
fgets(s, sizeof s, stdin); //读入整行
stringstream ss(s); //字符串流
string str;
while (ss >> str) { //在字符串流中读入字符串
if (mp.find(str) == mp.end()) mp[str] = ++c;
y = mp[str];
isRoot[y] = 0;
e[x].push_back(y); //建边(指向儿子)
}
}
f(i, 1, n) if (isRoot[i]) e[0].push_back(i); //超级根节点
a[0] = INF;
dfs(0);
ans = INF;
f(i, m, n) ans = min(ans, f[0][i]);
printf("%d\n", ans);
}
return 0;
}
H. Computer(换根树上 DP)
题意
给定一棵 \(n\) 个节点的树,边有权值 \(l\),求每个节点到其他节点的最远距离。
\(2\le n\le100000,\sum l\le10^9\)。
思路
(简单题,说得挺多,实际上没什么。。)
对于一个节点,这条最长的路径可能是向下到子树中的,也可能是向上经过父亲的。
我们设节点 \(i\) 向下到某一个叶子的最长路径为 \(dlf_i\)(distance to leaf),向上的最长路径为 \(dup_i\)。
那么节点 \(i\) 的答案即为 \(\max\{dlf_i,dup_i\}\)。
对于 \(dlf\),我们用一次 DFS 容易求得。具体来说,如果 \(v\) 是 \(u\) 的儿子,它们之间的边权为 \(w_{u,v}\),那么在 DFS 回溯之后更新 \(dlf_u\):
对于 \(dup\),我们再用一次 DFS 也可以求得。如果 \(v\) 是 \(u\) 的儿子,它们之间的边权为 \(w_{u,v}\),那么由 \(u\) 更新 \(v\),讨论从 \(u\) 向上还是向下:
但是!!!我们发现一个重要的 bug:如果长度为 \(dlf_u\) 的这条路径经过 \(v\) 怎么办?这时由 \(dlf_u\) 更新 \(dup_v\) 不是重复经过 \(v\) 了吗??
所以我们再保存一个由节点向下到叶子的所有路径的次大值,并且保存最大值是从哪个儿子更新来的。
设 \(dlf_{i,0}\) 表示由 \(i\) 向下到叶子的所有路径的最大值,\(dlf_{i,1}\) 表示由 \(i\) 向下到叶子的所有路径的次大值,\(son_i\) 表示更新 \(dlf_{i,0}\) 的 \(i\) 的那个儿子。
处理 \(dlf\) 的过程与上面类似,分别与当前的 \(dlf_{u,0}\) 和当前的 \(dlf_{u,1}\) 比较即可,过程中保存 \(son_u\)。\(dup_v\) 的状态转移要考虑 \(v\) 是否等于 \(son_u\)。
答案为 \(\max\{dlf_{i,0},dup_i\}\)。
代码
点击查看代码
#include <iostream>
#include <cstring>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
const int N = 1e5 + 10;
int n, dlf[N][2], son[N], dup[N];
struct Edge {
int to, nxt, val;
} e[N << 1];
int head[N], cnt;
inline void add(int from, int to, int val) {
e[++cnt].to = to, e[cnt].nxt = head[from], e[cnt].val = val, head[from] = cnt;
return;
}
void dfs1(int u, int fa) {
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, w = e[i].val;
if (v == fa) continue;
dfs1(v, u);
if (dlf[v][0] + w > dlf[u][0]) {
dlf[u][1] = dlf[u][0];
dlf[u][0] = dlf[v][0] + w, son[u] = v;
} else if (dlf[v][0] + w > dlf[u][1]) {
dlf[u][1] = dlf[v][0] + w;
}
}
}
void dfs2(int u, int fa) {
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, w = e[i].val;
if (v == fa) continue;
dup[v] = max(dup[u], (son[u] == v) ? dlf[u][1] : dlf[u][0]) + w;
dfs2(v, u);
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
while (cin >> n) {
memset(dlf, 0, sizeof dlf);
memset(dup, 0, sizeof dup);
memset(head, 0, sizeof head);
cnt = 0;
f(i, 2, n) {
int y, l;
cin >> y >> l;
add(i, y, l), add(y, i, l);
}
dfs1(1, 1);
dfs2(1, -1);
f(i, 1, n) cout << max(dlf[i][0], dup[i]) << '\n';
}
return 0;
}
*I. [HNOI2011]XOR和路径(有后效性 DP,高斯消元,期望)
洛谷 P3211 | AcWing 326 | 码创未来
TO DO...
J. Fence Obstacle Course(线段树优化 DP)
POJ 2374 | AcWing 329 | 码创未来
题意
农夫约翰为他的奶牛们建造了一个围栏障碍训练场,以供奶牛们玩耍。
训练场由 \(n\) 个不同长度的围栏组成,每个围栏都与 \(x\) 轴平行,并且第 \(i\) 个围栏的 \(y\) 坐标为 \(i\),\(x\) 坐标覆盖 \([a_i,b_i]\)。
训练场的出口位于原点 \((0,0)\),起点位于 \((S,n)\)。
这些牛会从起点处开始向下走,当它们碰到围栏时会选择沿着围栏向左或向右走,走到围栏端点时继续往下走,按照此种走法一直走到出口为止。
求出这些牛从开始到结束,行走的水平距离的最小值。
\(1\le n\le30000,−10^5\le S\le10^5,−10^5\le a_i,b_i\le10^5\)。
思路
为了方便转移,我们将从上往下走改为从下往上走,从 \((0,0)\) 最终走到 \((S,n)\)。如图所示。
设 \(f(i,j)\) 表示到第 \(i\) 层栅栏的左/右端点时的答案,其中 \(1\le i<n,j\in\{0,1\}\)。
初值:\(f(0,0)=f(0,1)=0\)。令 \(a_0=b_0=0\)。
设 \(lst(i)\) 表示上一个覆盖直线 \(x=i\) 的栅栏编号。
那么状态转移方程为:
答案为 \(\min\{f(n-1,0)+|a_{n-1}-S|,f(n-1,1)+|b_{n-1}-S|\}\)。
还有一个问题:如何快速求出 \(lst_i\)?
我们需要一个支持区间覆盖、单点查询的数据结构,线段树标记下传可以很好地解决。
代码
点击查看代码
#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
const int N = 5e4 + 10, M = 3e5 + 10;
int n, s, f[N][2], a[N], b[N];
inline int abs(int x) { return __builtin_abs(x); }
struct SegTree { //区间覆盖, 单点查询
#define lson (u << 1)
#define rson (u << 1 | 1)
struct Node {
int l, r, x;
} tr[M << 2];
inline void pushdown(int u) {
if (~tr[u].x) tr[lson].x = tr[rson].x = tr[u].x, tr[u].x = -1;
return;
}
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) return;
else tr[u].x = -1;
int mid = (l + r) >> 1;
build(lson, l, mid);
build(rson, mid + 1, r);
return;
}
void modify(int u, int l, int r, int x) {
if (l <= tr[u].l && tr[u].r <= r) return tr[u].x = x, void();
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) modify(lson, l, r, x);
if (r > mid) modify(rson, l, r, x);
return;
}
int query(int u, int x) {
if (tr[u].l == x && tr[u].r == x) return tr[u].x;
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (x <= mid) return query(lson, x);
else return query(rson, x);
}
} t;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> s;
s += 1e5;
t.build(1, 0, 2e5);
// t.modify(1, 0, 2e5, 0);
a[0] = 1e5, b[0] = 1e5;
// f[0][0] = f[0][1] = 0;
f(i, 1, n) {
cin >> a[i] >> b[i];
a[i] += 1e5, b[i] += 1e5;
int j = t.query(1, a[i]);
f[i][0] = min(f[j][0] + abs(a[i] - a[j]), f[j][1] + abs(a[i] - b[j]));
j = t.query(1, b[i]);
f[i][1] = min(f[j][0] + abs(b[i] - a[j]), f[j][1] + abs(b[i] - b[j]));
t.modify(1, a[i], b[i], i);
}
cout << min(f[n][0] + abs(a[n] - s), f[n][1] + abs(b[n] - s)) << '\n';
return 0;
}
*K. [USACO09OPEN]Tower of Hay G(单调队列优化 DP)
洛谷 P4954 | AcWing 331 | 码创未来
题意
一共有 \(n\) 大包的干草(从 \(1\) 到 \(n\) 编号),第 \(i\) 包干草有一个宽度 \(w_i\)。所有的干草包的厚度和高度都为 \(1\)。
Bessie 必须利用所有 \(n\) 包干草来建立起干草堆。具体来说,她可以在一层中放若干包干草,但是这些干草只能紧挨着放在一起,并且总宽度不超过下面一层(最下面一层的宽度无限制)。她持续像这样堆放干草,直到所有的草包都被安置完成。
她必须按照从 \(1\) 到 \(n\) 的顺序堆放干草包。
Bessie 的目标是建立起最高的干草堆。求出最高的干草堆的高度。
\(1\le N\le100000,1\le w_i\le10000\)。
思路
由于从下到上搭干草堆不好维护,我们考虑从上到下搭干草堆。那么应依次读入 \(n\) 到 \(1\) 号的干草包。
猜想:如果使底层宽度最小,那么一定可以构造出一种层数最高的方案。
证明:对于当前的所有干草,任取一个能使层数最高的方案,设有 \(C_A\) 层,把其中从下往上每一层最大的块编号记为 \(A_i\);任取一个能使底边最短的方案,设有 \(C_B\) 层,把其中从下往上每一层最大的块编号记为 \(B_i\)。显然 \(A_1\ge B_1,A_{C_B}\le B_{C_B}\),这说明至少存在一个 \(k\in(1,C_B)\),满足 \(A_{k-1}\ge B_{k-1}\) 且 \(A_k\le B_k\)。也就是说,方案 \(A\) 第 \(k\) 层完全被方案 \(B\) 第 \(k\) 层包含。构造一个新方案,第 \(k\) 层往上按方案 \(A\),往下按方案 \(B\),两边都不要的块放中间当第 \(k\) 层。新方案的层数与 \(A\) 相同,而底边长度与 \(B\) 相同。证毕。(proof by zkw)(引自 USACO 2009 Open 干草塔Tower of Hay 解题报告 - Lazycal - 博客园)
设 \(f(i)\) 表示用第 \(1\) 到 \(i\) 包干草最多能搭多少层。初值 \(f(0)=0\),答案为 \(f(n)\)。
设 \(g(i)\) 表示从上到下第 \(i\) 层的最小宽度。
状态转移方程:
含义是:枚举 \(j\),把 \(j+1\) 到 \(i\) 的这些干草放到当前这一层(如果能放的话)。只是暴力枚举 \(j\) 的话,复杂度是 \(O(n^2)\) 的,所以我们要进一步优化。
设 \(w\) 的前缀和数组为 \(s\)。条件可以表示为:\(s_i-s_j>g(j)\)。注意到如果移项变为 \(g(j)+s_j<s_i\),那么要维护的多项式只与 \(j\) 有关,并且随着 \(j\) 递增, \(g(j)\) 和 \(s_j\) 都单调递增。所以为了找到最大的(因为要更新 \(g(j)\))满足上述不等式的 \(j\),我们维护一个 \(g(j)+s_j\) 单调递增,下标单调递增的单调队列。那么每次弹出队首直至不满足条件,最后一个满足条件的队首就是满足条件的最大的 \(j\)。用 \(f(j)+1\) 更新 \(f(i)\),并且用 \(s_i-s_j\) 更新 \(g(j)\)。
总之这道题的特殊之处在于不是直接取队首,而是取最后一个满足条件的队首。
代码
点击查看代码
#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
using namespace std;
const int N = 1e5 + 10;
int n, w[N], s[N], q[N], h, t, f[N], g[N], ans;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
g(i, n, 1) cin >> w[i];
h = t = 1;
f[0] = 0;
f(i, 1, n) {
s[i] = s[i - 1] + w[i];
while (h <= t && g[q[h]] + s[q[h]] <= s[i]) ++h;
int j = q[h - 1];
f[i] = f[j] + 1;
ans = max(ans, f[i]);
g[i] = s[i] - s[j];
while (h <= t && g[q[t]] + s[q[t]] >= g[i] + s[i]) --t;
q[++t] = i;
}
cout << ans << '\n';
return 0;
}

浙公网安备 33010602011771号