@codeforces - 739E@ Gosha is hunting
@description - translation@
Gosha 的目标是成为宝可梦大师。现在有 n 个宝可梦, Gosha 手里有 a 个普通球与 b 个 超级球。已知第 i 个宝可梦被普通球捕获的概率 pi 与被超级球捕获的概率 ui。Gosha 先决定哪些宝可梦扔普通球,哪些扔超级球;如果一个宝可梦同时被 Gosha 扔了两个球,则如果它被任何一个抓捕都算作被捕获,捕获后另外一个球依然消耗掉。
Gosha 想知道他在最优决策下,抓捕到的宝可梦的最大期望数量。
input
第一行包含三个整数 n a b(2 <= n <= 2000, 0 <= a, b <= n)。
第二行包含 n 个实数 p1, p2, p3, ..., pn(0 <= pi <= 1)。
第三行包含 n 个实数 u1, u2, u3, ..., un(0 <= ui <= 1)。
output
输出最大期望数量。
simple input
3 2 2
1.000 0.000 0.500
0.000 1.000 0.500
simple output
2.75
@solution - version - 1@
CF 给的正解好像是 dp 加鬼畜的优化,但是这道题也存在网络流的做法。
UPD in 2019/1/3:官方题解并不是 dp 而是贪心的样子。
考虑第 i 只宝可梦的四类决策:
(1)不扔球,对答案的贡献为 0。
(2)扔普通球,对答案的贡献为 pi。
(3)扔超级球,对答案的贡献为 ui。
(4)双球齐下,对答案的贡献为 1 - (1 - pi)(1 - ui) = pi + ui - pi*ui。
球的个数限制可以用边的容量去限制:建两个新点 P 与 U,源点向他们分别连容量为 a,b,费用为 0 的边。
然后对于每一个宝可梦建一个点,P, U 连第 i 只宝可梦容量为 1,费用为 pi, ui 的边。
宝可梦怎么向汇点连边呢?假如只运了一个流过来,对应情况(3),(4),此时产生的费用是 pi/ui ,费用不需要再变化;假如运了两个流过来,对应情况(4),此时产生的费用是 pi + ui,我们需要费用再减去 pi*ui。
有点像 “你多运输几个流过来才能解锁新的费用” 的意思【个人总结 www 很奇特的比喻】。
对于这样的题,有一个套路就是:连两条边,使得第一条比第二条优,且第一条的容量有限。
具体到这道题,我们每只宝可梦向汇点连两条边:费用为 0,容量为 1;费用为 -pi*ui,容量为 1。因为我们是求解的最大期望,所以流肯定先往费用为 0 的边跑,再往费用为 1 的边跑,这样就实现了我们的目标。
@accepted code - version - 1@
因为是求解的最大费用流,所以上文提到的所有费用对应到代码中全部取反 qwq。
#include<cstdio>
#include<queue>
#include<cmath>
using namespace std;
const int MAXN = 2000;
const int MAXV = 2*MAXN;
const int MAXE = 5*MAXN;
const int INF = (1<<30);
const double EPS = 1E-7;
bool cmp(double a, double b) {
return a - b < -EPS;
}
bool equal(double a, double b) {
return -EPS < a - b && a - b < EPS;
}
struct FlowGraph{
struct edge{
int to, cap, flow; double dis;
edge *nxt, *rev;
}edges[2*MAXE + 5], *cur[MAXV + 5], *adj[MAXV + 5], *ecnt;
int S, T; double mindis, cost, dist[MAXV + 5]; bool vis[MAXV + 5], inq[MAXV + 5];
deque<int>que;
void init() {
ecnt = &edges[0];
}
void restore() {
for(int i=S;i<=T;i++)
dist[i] = INF, cur[i] = adj[i];
}
void addedge(int u, int v, int c, double w) {
edge *p = (++ecnt);
p->to = v, p->cap = c, p->dis = w, p->flow = 0;
p->nxt = adj[u], adj[u] = p;
edge *q = (++ecnt);
q->to = u, q->cap = 0, q->dis = -w, q->flow = 0;
q->nxt = adj[v], adj[v] = q;
q->rev = p, p->rev = q;
}
bool relabel() {
que.push_back(S); inq[S] = true; dist[S] = 0;
while( !que.empty() ) {
int f = que.front(); que.pop_front();
for(edge *p=adj[f];p!=NULL;p=p->nxt) {
if( p->cap > p->flow && cmp(dist[f] + p->dis, dist[p->to]) ) {
dist[p->to] = dist[f] + p->dis;
if( !inq[p->to] ) {
if( !que.empty() && cmp(dist[p->to], dist[que.front()])) que.push_front(p->to);
else que.push_back(p->to);
}
}
}
}
mindis = dist[T];
return !(dist[T] == INF);
}
int aug(int x, int tot) {
if( x == T ) {
cost += mindis*tot;
return tot;
}
int sum = 0; vis[x] = true;
for(edge *&p=cur[x];p!=NULL;p=p->nxt) {
if( !vis[p->to] && p->cap > p->flow && equal(dist[x] + p->dis, dist[p->to]) ) {
int del = aug(p->to, min(p->cap-p->flow, tot-sum));
p->flow += del, p->rev->flow -= del, sum += del;
if( sum == tot ) break;
}
}
vis[x] = false;
return sum;
}
int min_cost_max_flow() {
int flow = 0; restore();
while( relabel() )
flow += aug(S, INF), restore();
return flow;
}
}G;
double p[MAXN + 5], u[MAXN + 5];
int main() {
int n, a, b; G.init();
scanf("%d%d%d", &n, &a, &b);
for(int i=1;i<=n;i++)
scanf("%lf", &p[i]);
for(int i=1;i<=n;i++)
scanf("%lf", &u[i]);
G.S = 0, G.T = 2+n+1;
G.addedge(G.S, n+1, a, 0), G.addedge(G.S, n+2, b, 0);
for(int i=1;i<=n;i++) {
G.addedge(n+1, i, 1, -p[i]), G.addedge(n+2, i, 1, -u[i]);
G.addedge(i, G.T, 1, 0), G.addedge(i, G.T, 1, p[i]*u[i]);
}
int ans = G.min_cost_max_flow();
printf("%lf\n", -G.cost);
}
@solution - version - 2@
【UPD in 2019/1/3】
可以发现,最优情况下,我们必定将所有球用完。
所以我们将问题转换为:恰好用 a 个普通球与 b 个超级球的情况下,我们能取到的最大期望值。
满眼都是带权二分的套路感。
但是我们有两个限制个数的量怎么办?
当然是二分套二分啦!
二分使用一个球的代价,代价越大用球越少,代价越小用球越多。
上界为 1:即使抓住了一个,期望减一下就成非正数了,所以一个都不用。
下界为 0:用球不花费,随便用。
注意此题因为概率是实数,所以必须使用实数二分。
其实这道题的凸性很容易证明:因为我们要最优解,所以肯定是先扔概率大的再扔概率小的,所以最优解的增长趋势会越来越缓慢,所以就是个凸的。
@accepted - version - 2@
#include<cstdio>
const int MAXN = 2000;
double p[MAXN + 5], u[MAXN + 5];
double ans;
int n, a, b, cnt1, cnt2;
bool Check2(double x, double y) {
cnt1 = cnt2 = ans = 0;
for(int i=1;i<=n;i++) {
int type = 0; double tmp = ans;
if( ans + p[i] - x > tmp )
tmp = ans + p[i] - x, type = 1;
if( ans + u[i] - y > tmp )
tmp = ans + u[i] - y, type = 2;
if( ans + p[i] + u[i] - p[i]*u[i] - x - y > tmp )
tmp = ans + p[i] + u[i] - p[i]*u[i] - x - y, type = 3;
if( type & 1 ) cnt1++;
if( type & 2 ) cnt2++;
ans = tmp;
}
return cnt2 >= b;
}
bool Check1(double x) {
double le = 0, ri = 1;
for(int i=1;i<=50;i++) {
double mid = (le + ri) / 2;
if( Check2(x, mid) ) le = mid;
else ri = mid;
}
Check2(x, le); ans += le*b;
return cnt1 >= a;
}
int main() {
scanf("%d%d%d", &n, &a, &b);
for(int i=1;i<=n;i++)
scanf("%lf", &p[i]);
for(int i=1;i<=n;i++)
scanf("%lf", &u[i]);
double le = 0, ri = 1;
for(int i=1;i<=50;i++) {
double mid = (le + ri) / 2;
if( Check1(mid) ) le = mid;
else ri = mid;
}
Check1(le);
printf("%lf\n", ans + le*a);
}
@details@
本题听机房里的人说,不用 EPS 过不了,EPS 取太大也过不了。
诶嘿嘿谢谢了,起码我不用调试那么久了 qwq。
UPD in 2019/1/3:这么多非官方的正解都能过……
甚至还比官方优秀 2333。