题目归档 #3
目录
- [SCOI2008] 奖励关
- [HAOI2010] 软件安装
- [HDU3853] LOOPS
[SCOI2008] 奖励关
一道综合 状态压缩 和 概率期望 的 dp 好题。
题意
系统将依次随机抛出 \(k\) 次宝物,每次你都可以选择吃或者不吃(必须在抛出下一个宝物之前做出选择,且现在决定不吃的宝物以后也不能再吃)。
宝物一共有 \(n\) 种,系统每次抛出这 \(n\) 种宝物的概率都相同且相互独立。获取第 \(i\) 种宝物将得到\(P_i\) 分,但并不是每种宝物都是可以随意获取的。第 \(i\) 种宝物有一个前提宝物集合 \(S_i\)。只有当\(S_i\) 中所有宝物都至少吃过一次,才能吃第 \(i\) 种宝物。\(P_i\) 可以是负数。
假设你采取最优策略,平均情况你一共能在奖励关得到多少分值?
- \(k \leq 100,n \leq 15\)
解题
看到 \(n \leq 15\),第一反应必然是状态压缩。
于是想到令 \(f[i][j]\) 表示当前在第 \(i\) 次选择,前面 \(i-1\) 次及以前选择吃的宝物的集合在二进制下为 \(j\)。
我们可以开始写转移方程。但是这会遇到一个问题:你并不知道用 \(i-1\) 次能否达到 \(j\) 这个状态。为了避免程序复杂,我们可以考虑期望的一个惯用套路—— 逆推。
令 \(f[i][j]\) 表示当前在第 \(i\) 次选择,前面 \(i-1\) 次及以前选择吃的宝物的集合在二进制下为 \(j\) 时,第 \(i+1\) 到第 \(K\) 次选择的最优策略下期望得分。
第三维枚举当前第 \(i\) 次的宝物 \(t\)。如果状态 \(j\) 满足 \(t\) 的相关条件,正着看,我们就可以选择 \(t\) 了。此时有转移方程(\(val[t]\) 表示 \(t\) 的分值):
\(f[i][j] += max(f[i + 1][j], f[i + 1][j | (1 << (t-1))] + val[t]) / N\)
如果状态 \(j\) 不满足 \(t\) 的条件,那就没有办法选 \(t\) 咯。这个时候只有继承 \(i+1\) 的答案,因为在无法选择的情况下, \(1 \sim i-1\) 与 \(1 \sim i\) 的选择集合一定都一样了。因此此时有转移方程:
\(f[i][j] += f[i+1][j] / N\)
最后的答案,想必就是 \(f[1][0]\) 了吧。
程序
#include <iostream>
#include <cstring>
#include <cstdio>
#define Maxn 15
#define Maxk 105
using namespace std;
int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1;
c = getchar();
}
while('0' <= c && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
return x * f;
}
int K, N, val[Maxn], req[Maxn];
double f[Maxk][1 << Maxn];
int main() {
K = read(); N = read();
for(int i = 1; i <= N; ++i) {
val[i] = read();
int q = read();
while(q) {
req[i] += (1 << (q - 1));
q = read();
}
}
for(int i = K; i >= 1; --i) {
for(int j = 0; j < (1 << N); ++j) {
for(int t = 1; t <= N; ++t) {
int nowt = (1 << (t - 1));
if((j | req[t]) == j) {
if(j & nowt) f[i][j] += max(f[i + 1][j], f[i + 1][j] + val[t]);
else f[i][j] += max(f[i + 1][j], f[i + 1][j | nowt] + val[t]);
}
else f[i][j] += f[i + 1][j];
}
f[i][j] = f[i][j] * 1.0 / N;
}
}
printf("%.6lf", f[1][0]);
return 0;
}
[HAOI2010] 软件安装
一道比较套路的树上背包型 dp 题目,写起来有点小麻烦。可以视作「选课」那道题的加强版。
题意
\(N\) 个软件。软件 \(i\),它要占用 \(W_i\) 的磁盘空间,价值为 \(V_i\) 。从中选择一些软件安装到一台磁盘容量为 \(M\) 计算机上,使得这些软件的价值尽可能大。
软件之间存在依赖关系,即软件 \(i\) 只有在安装了软件 \(D_i\) 并且 \(D_i\) 正常工作的情况下才能正确工作。一个软件最多依赖另外一个软件。无法正常工作的软件可以直接无视。
求最大软件价值和。
- \(N \leq 100,M \leq 500\)。
解题
首先明显看出,这是一棵由「内向树」和「树」组成的森林。显然必须把那些环干掉,考虑到一个环必须一起选,我们可以找出所有的环并把他们的 \(W,V\) 都相加。缩为一个点后分配一个编号,与原先连到这个环上的点相连。建完新图后跑一边树上背包即可。
找环可以用 tarjan,这里我使用的是 topsort。
程序
#include <iostream>
#include <cstring>
#include <cstdio>
#define Maxn 110
#define Maxm 510
using namespace std;
const int S = 0;
int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1;
c = getchar();
}
while('0' <= c && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
return x * f;
}
int N, M, W[Maxn], V[Maxn], D[Maxn], deg[Maxn];
struct Edge {
int next, to;
}
edge[Maxn * 2];
int head[Maxn], edge_num;
void add_edge(int from, int to) {
edge[++edge_num].next = head[from];
edge[edge_num].to = to;
head[from] = edge_num;
}
int stack[Maxn], top, inl[Maxn], isr[Maxn];
void fl(int u, int id) {
inl[u] = id;
for(int i = head[u]; i != -1; i = edge[i].next) {
int v = edge[i].to;
if(!inl[v]) W[id] += W[v], V[id] += V[v], fl(v, id);
}
}
void topsort() {
for(int i = 1; i <= N; ++i) {
if(!deg[i]) stack[++top] = i;
}
while(top) {
int u = stack[top--];
for(int i = head[u]; i != -1; i = edge[i].next) {
int v = edge[i].to;
--deg[v];
if(!deg[v]) stack[++top] = v;
}
}
for(int i = 1; i <= N; ++i) {
if(deg[i] && !inl[i]) isr[i] = 1, fl(i, i);
else if(!inl[i]) inl[i] = i;
}
}
void rebuild() {
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
edge_num = 0;
for(int i = 1; i <= N; ++i) if(inl[D[i]] != inl[i]) add_edge(inl[D[i]], inl[i]);
for(int i = 1; i <= N; ++i) if(isr[i]) add_edge(S, i);
}
int f[Maxn][Maxm];
void dfs(int u) {
// cout << endl << u << " ";
for(int i = head[u]; i != -1; i = edge[i].next) {
int v = edge[i].to;
dfs(v);
}
for(int i = W[u]; i <= M; ++i) f[u][i] = V[u];
for(int i = head[u]; i != -1; i = edge[i].next) {
int v = edge[i].to;
for(int j = M; j >= W[u] + 1; --j) {
for(int k = 0; k <= j - W[u]; ++k) {
f[u][j] = max(f[u][j], f[v][k] + f[u][j - k]);
}
}
}
}
int main() {
N = read(); M = read();
memset(head, -1, sizeof(head));
for(int i = 1; i <= N; ++i) W[i] = read();
for(int i = 1; i <= N; ++i) V[i] = read();
for(int i = 1; i <= N; ++i) {
D[i] = read();
add_edge(i, D[i]);
++deg[D[i]];
}
topsort();
rebuild();
dfs(S);
int ans = 0;
for(int j = 0; j <= M; ++j) ans = max(ans, f[0][j]);
cout << ans << endl;
return 0;
}
[HDU3853] LOOPS
一道比较基础的期望 dp 题目。
题意
有一个 \(R \times C\) 的迷宫,从 \((1,1)\) 走到 \((R,C)\),每个格子给出停留在原地,向右走一格和向下走一格的概率,且每走一步需要 \(2\) 点能量,求最后所需要的能量期望。
- \(R,C \leq 1000\)
解题
显然这道题适合 逆推。
令 \(f[i][j]\) 表示从 \((i,j)\) 走到 \((R,C)\) 的期望次数(\(2\) 放到最后来乘)。假设此处停留、向下、向右的概率分别为 \(sto_{i,j},dow_{i,j},lef_{i,j}\),显然可以列出方程如下:
\(f[i][j] = sto_{i,j} \times f[i][j] + dow_{i,j} \times f[i+1][j] + lef_{i,j} \times f[i][j+1] +1\)
移项即得 \(f[i][j] = \frac {dow_{i,j} \times f[i+1][j] + lef_{i,j} \times f[i][j+1] +1}{1-sto_{i,j}}\)
然后就可以开始愉快地 dp 了。另外不知道为什么这道题遇到 \(sto_{i,j}=1\) (也就是在此处死循环)的时候要直接 continue;,难道期望不就应该变成 \(\infty\) 吗。。。
程序
#include <iostream>
#include <cstring>
#include <cstdio>
#define Maxn 1010
using namespace std;
int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1;
c = getchar();
}
while('0' <= c && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
return x * f;
}
int N, M;
double sto[Maxn][Maxn], dow[Maxn][Maxn], lef[Maxn][Maxn], f[Maxn][Maxn];
int main() {
while(scanf("%d%d", &N, &M) != EOF) {
memset(sto, 0, sizeof(sto));
memset(dow, 0, sizeof(dow));
memset(lef, 0, sizeof(lef));
memset(f, 0, sizeof(f));
for(int i = 1; i <= N; ++i) {
for(int j = 1; j <= M; ++j) {
scanf("%lf", &sto[i][j]);
scanf("%lf", &lef[i][j]);
scanf("%lf", &dow[i][j]);
}
}
for(int i = N; i >= 1; --i) {
for(int j = M; j >= 1; --j) {
if(i == N && j == M) continue;
if(sto[i][j] >= 0.999999) continue;
f[i][j] = (f[i + 1][j] * dow[i][j] + f[i][j + 1] * lef[i][j] + 1) / (1 - sto[i][j]);
}
}
printf("%.3lf\n", f[1][1] * 2);
}
return 0;
}

浙公网安备 33010602011771号