CONTENT: DP - II
BB
最近Practice系列的,所以等有一定量再更新,当然一个月多没更新是因为我在划水 QAQ
Let the light guide us - HDU 3698
Link
https://cn.vjudge.net/problem/HDU-3698
Description
给定t[i][j]与f[i][j],在满足每一行只选一个t[i][j],且满足|j - k| <= f[i][j] + f[i - 1][k]
的情况下,t[i][j]之和最小值。本题范围为2<=N<=100 , 1<=M<=5000, 0<=Tij, Fij<=100000
Sample Input
3 5
9 5 3 8 7
8 2 6 8 9
1 9 7 8 6
0 1 0 1 2
1 0 2 1 1
0 2 1 0 2
0 0
Sample Output
10
Solution —— DP + 线段树优化
首先,本题的DP方程是明显的,即定义dp(i, j)
为前i行在第i行选第j个的最小值,那么
dp(i, j) = t(i, j) + min{ dp(i - 1, k) | |j - k| <= f(i, j) + f(i - 1, k) } (1 <= k <= m)
但是复杂度高达O(nm^2),是不可接受的
上述dp的瓶颈在于需要枚举k确定dp(i - 1, k)
的最小值,而需要枚举的原因是约束不等式
现在拆这个不等式,可得-f(i, j) - f(i - 1, k) <= j - k <= f(i, j) + f(i - 1, k)
,可得j - f(i - j) <= f(i - 1, k) + k
且j + f(i, j) >= k - f(i - 1, k)
,即区间[j - f(i, j), j + f(i, j)]
与区间[k - f(i - 1, k), k + f(i - 1, k)]
有交集
因此当dp到第i层时,用线段树把上一层的[k - f(i - 1, k), k + f(i - 1, k)]
区间置为dp所能达到的最小值,那么
dp(i, j) = t(i + j) + query([j - f(i, j), j + f(i, j)])
总复杂度O(nm’log(m’))
#include <bits/stdc++.h>
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
using namespace std;
typedef long long ll;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int M = 5015;
const int N = 1e5 + M;
ll seg[N << 2], lzy[N << 2], dp[M];
int tim[105][M], f[105][M];
int maxd[105];
inline char get(void) {
static char buf[1000000];
static char* p1 = buf;
static char* p2 = buf;
if (p1 == p2) {
p2 = (p1 = buf) + fread(buf, 1, 1000000, stdin);
if (p1 == p2) return EOF;
}
return (*p1++);
}
inline int read() {
int x = 0; static char c; bool minus = false;
for (; !(c >= '0' && c <= '9'); c = get()) if (c == '-') minus = true;
for (; c >= '0' && c <= '9'; x = x * 10 + c - '0', c = get()); if (minus) x = -x;
return x;
}
inline void pushUp(int rt) {
seg[rt] = min(seg[rt << 1], seg[rt << 1 | 1]);
}
inline void pushDown(int rt) {
if(lzy[rt] != inf) {
lzy[rt << 1] = min(lzy[rt << 1], lzy[rt]);
lzy[rt << 1 | 1] = min(lzy[rt << 1 | 1], lzy[rt]);
seg[rt << 1] = min(seg[rt << 1], lzy[rt]);
seg[rt << 1 | 1] = min(seg[rt << 1 | 1], lzy[rt]);
lzy[rt] = inf;
}
}
inline void update(int ql, int qr, ll val, int l, int r, int rt) {
if(ql <= l && r <= qr) {
seg[rt] = min(seg[rt], val);
lzy[rt] = min(lzy[rt], val);
return;
}
pushDown(rt);
int m = (l + r) >> 1;
if(ql <= m) {
update(ql, qr, val, lson);
}
if(m < qr) {
update(ql, qr, val, rson);
}
pushUp(rt);
}
inline ll query(int ql, int qr, int l, int r, int rt) {
if(ql <= l && r <= qr) {
return seg[rt];
}
pushDown(rt);
int m = (l + r) >> 1;
ll ans = inf;
if(ql <= m) {
ans = min(ans, query(ql, qr, lson));
}
if(m < qr) {
ans = min(ans, query(ql, qr, rson));
}
return ans;
}
int main() {
int n, m;
while(true) {
memset(maxd, 0, sizeof(maxd));
n = read();
m = read();
if(n == 0 && m == 0) {
break;
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
tim[i][j] = read();
}
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
f[i][j] = read();
maxd[i] = max(maxd[i], f[i][j] + j + 5);
}
}
for(int j = 1; j <= m; j++) {
dp[j] = tim[1][j];
}
for(int i = 2; i <= n; i++) {
memset(seg, 0x3f, 4 * maxd[i] * sizeof(ll));
memset(lzy, 0x3f, 4 * maxd[i] * sizeof(ll));
for(int k = 1; k <= m; k++) {
update(max(k - f[i - 1][k], 0) + 1, f[i - 1][k] + k + 1, dp[k], 1, maxd[i], 1);
}
for(int j = 1; j <= m; j++) {
dp[j] = tim[i][j] + query(max(j - f[i][j], 0) + 1, j + f[i][j] + 1, 1, maxd[i], 1);
}
}
ll ans = inf;
for(int i = 1; i <= m; i++) {
ans = min(ans, dp[i]);
}
printf("%lld\n", ans);
}
}
GeoDefense - HDU 4044
Link
https://cn.vjudge.net/problem/HDU-4044
Description
给定一棵树,以1为根,在每个点都有ki
种炮塔可供选择,每种炮塔为(price, power)
,每个点最多只能建一个炮塔,先给定初始price
,问在建造炮塔总price
不超过所给定初始price
的情况下,根到叶子节点的每条路径上的power
之和中的最小值,要求这个值最大
Sample Input
2
2
1 2
30
3 10 20 20 40 30 50
3 10 30 20 40 30 45
4
2 1
3 1
1 4
60
3 10 20 20 40 30 50
3 10 30 20 40 30 45
3 10 30 20 40 30 35
3 10 30 20 40 30 35
Sample Output
70
80
Solution —— 树形DP + 分组背包
定义dp(u, mon)
为点u
及其子树在总price为mon
的情况下,u
到叶子节点的每条路径的最大最小值,那么
dp'(u, mon) = (each v = son(u), knapsackMax{ dp(v, monv), 1 <= monv <= mon })
dp(u, mon) = (each tower, knapsackMax { power + dp'(u, mon - price) })
对于第一个方程,对u
的每一个分支做分组背包,选其中丢进容量为mon
的背包后能使得最小值最大的一个,那么最后dp(u, mon)
就是本身不建炮塔时的最优方案
对于第二个方程,考虑建炮塔,把炮塔丢进去做分组背包,选值最大的一个
虽然列了两个方程,但实际上开一个dp数组就行
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e3 + 15;
const int M = 205;
const int inf = 0x3f3f3f3f;
struct edge {
int v, nxt;
};
int head[N], tot;
edge e[N << 1];
vector<int> kmon[N], kden[N];
int dp[N][M];
inline void init() {
memset(head, -1, sizeof(head));
memset(dp, 0, sizeof(dp));
tot = 0;
}
inline void addEdge(int u, int v) {
e[tot] = edge{v, head[u]};
head[u] = tot++;
}
void dfs(int u, int pre) {
bool isFirst = true;
for(int i = head[u]; ~i; i = e[i].nxt) {
int v = e[i].v;
if(v == pre) {
continue;
}
dfs(v, u);
if(isFirst) {
for(int mon = 0; mon < M; mon++) {
dp[u][mon] = dp[v][mon];
}
isFirst = false;
continue;
}
for(int mon = M - 1; mon >= 0; mon--) {
int maxx = 0;
for(int monv = 0; monv <= mon; monv++) {
maxx = max(maxx, min(dp[u][mon - monv], dp[v][monv]));
}
dp[u][mon] = maxx;
}
}
for(int mon = M - 1; mon >= 0; mon--) {
int maxx = dp[u][mon];
for(int i = 0; i < (int)kmon[u].size(); i++) {
if(kmon[u][i] <= mon) {
maxx = max(maxx, dp[u][mon - kmon[u][i]] + kden[u][i]);
}
}
dp[u][mon] = maxx;
}
}
int main() {
int t;
scanf("%d", &t);
while(t--) {
init();
int n, mon;
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
kmon[i].clear();
kden[i].clear();
}
for(int i = 1; i <= n - 1; i++) {
int u, v;
scanf("%d%d", &u, &v);
addEdge(u, v);
addEdge(v, u);
}
scanf("%d", &mon);
for(int i = 1; i <= n; i++) {
int k;
scanf("%d", &k);
while(k--) {
int mon, den;
scanf("%d%d", &mon, &den);
kmon[i].emplace_back(mon);
kden[i].emplace_back(den);;
}
}
dfs(1, -1);
printf("%d\n", dp[1][mon]);
}
}
Bookshelves - CodeForces 981D
Link
https://cn.vjudge.net/problem/CodeForces-981D
Description
给定一个长度为n
的序列,要求分成k
段,求每段之和相与的最大值
Sample Input
10 4
9 14 28 1 7 13 15 29 2 31
7 3
3 14 15 92 65 35 89
Sample Output
24
64
Solution —— 贪心 + DP + 枚举 + 动态维护
(本题为jjdl钦点题目!)
首先需要肯定一点,两段之和分别取最大值再相与不能取得所求值,因为可能有一段的值为1000,另一段为0111,相与后答案为0,而左边可能能取到0111,相与后为0111
通过上述例子可以发现,实际上二进制与的决定因素在高位,因此考虑从高位向低位贪心DP,但问题是如何保留上一次的结果?
(翻了一下Solution后,划掉)可以采用枚举假设的方法维护最大值,现在从高位出发,假设高位为1,那么此时的值为100…000,记为ans
,然后DP,设dp(i, k)
为前i
个数分为k
段高位能否为ans
,那么
dp(i, k) = or{ dp(j, k - 1) and (sum(j + 1, i) & ans == ans) }
如果最终dp(n, k)
为true,那么保留这一位为1的结果,否则置为0,那么下一次dp便可以利用之前dp的结果
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 75;
ll sum[N];
bool ok[N][N];
bool isCan(int n, int kk, const ll& ans) {
memset(ok, false, sizeof(ok));
for(int i = 1; i <= n; i++) {
ok[i][1] = ((sum[i] & ans) == ans);
}
for(int k = 2; k <= kk; k++) {
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= i - 1; j++) {
ok[i][k] |= (ok[j][k - 1] & (((sum[i] - sum[j]) & ans) == ans));
}
}
}
return ok[n][kk];
}
int main() {
int n, k;
while(~scanf("%d%d", &n, &k)) {
for(int i = 1; i <= n; i++) {
scanf("%lld", &sum[i]);
sum[i] += sum[i - 1];
}
ll ans = 0;
for(int p = 60; p >= 0; p--) {
ans = ans | (1LL << p);
if(!isCan(n, k, ans)) {
ans ^= (1LL << p);
}
}
printf("%lld\n", ans);
}
}