Samara Farewell Contest 2020 (XXI Open Cup, GP of Samara) 部分题目解答
传送门:https://codeforces.com/gym/102916
D
题意:给定 \(n\) 个物品,价值分别为 \(w_{i}\) ,AB两人轮流从物品中选取物品,A会从中选价值最大的,而B在物品中随机选一个。求A,B得到物品价值的期望。
分析:
概率DP
直接按照题意解可能较难(一个个取物品),故考虑反向操作(A、B轮流放标记),记A放的标记为黑色,而B放的标记为白色。当 \(n\) 个标记的已经放好后,对应的序列就指示着AB选取的物品。
例如前 \(i\) 个标记已经放好了,相应的序列是白黑黑白黑
,则意味着A取走第 \(2,3,5\) 个物品。
我们用 \(f[i,j]\) 表示 \(i\) 个标记已经放好,其中 \(i\) 个标记里第 \(j\) 个为黑色的概率。
下求递推方程:放置第 \(i\) 个标记时
-
若现在轮到A:
则A一定会将标记放在序列的最后,故 \(f[i,j]=f[i-1,j]~(j<i)\) 且 \(f[i,i]=1\) -
若现在轮到B:
B会将标记随机插入到一个位置,假设插入前第 \(p\) 个标记是黑色,那么当前标记插入到其左边的概率是 \(\frac{p-1}{i}\),插入后刚好取代黑色标记的位置的概率是 \(\frac{1}{i}\),插入到其右边的概率是 \(\frac{i-p}{i}\)
相应的方程为: \(f[i,j]=\frac{j-1}{i}f[i-1,j-1]+\frac{1}{i}0+\frac{i-p}{i}f[i-1,j]\)
当 \(n\) 是偶数的时候,B是先手,反之A是先手,这样可以保证A得到最大值。
代码如下:
#pragma GCC optimize("O3")
#include<bits/stdc++.h>
using namespace std;
#define SET0(a) memset(a,0,sizeof(a))
#define FOR(i,a,b) for(int i=(a);i<=(b);i++)
#define DWN(i,a,b) for(int i=(a);i>=(b);i--)
#define INF 0x3f3f3f3f
typedef long long ll;
const int N=5005;
double f[N][N];
int n;
double w[N];
int main(){
cin>>n;
double sum=0;
FOR(i,1,n) {
cin>>w[i];
sum+=w[i];
}
sort(w+1,w+1+n);
bool ok=n&1;
FOR(i,1,n){
FOR(j,1,i)
if(ok){
f[i][j]=f[i-1][j];
f[i][i]=1;
}else{
f[i][j]=(j-1)*1.0/i*f[i-1][j-1]+
(i-j)*1.0/i*f[i-1][j];
}
ok=!ok;
}
double ans=0;
FOR(i,1,n) ans+=f[n][i]*1.0*w[i];
printf("%.12lf %.12lf",ans,sum-ans);
return 0;
}
K
注意到补的血不能超过体力上限 \(m\) ,故需要在读入的时候对 \(h\) 取 \(min(h,m)\) ,而又因为体力上限的存在,对于 \(h \geqslant t\) 的怪物,我们可以将其杀到只剩下一滴血然后在自己只剩一滴血的时候再杀它补血,可以证明这样能够充分利用血而不造成浪费,相当于自己的血量 \(hp=\sum (h-t)+m\) ,剩下的是 \(h<t\) 的怪物:
首先,对于这样的怪物,应该一个个地杀死(可以用反证法证明这样做是最优的),故下面求一个最优的击杀顺序:
考虑相邻的两个 \(i,i+1\) ,按顺序将它们杀死,自己的hp应该是:
\(hp-t_{i}+h_{i}-t_{i+1}+h_{i+1}\)
下面考察两个自己可能去世的地方:
- \(hp-t_{i}\)
- \(hp-t_{i}+h_{i}-t_{i+1}\)
对于第一个地方:无论是 \(i\) 还是 \(i+1\) 排在前面,只要存在一个满足其 \(t>hp\) 的都会去世(无论排序方案如何)。下考虑 \(t \leqslant hp\) 的情况
此时第一个地方已经不会导致自己去世,故可以考察第二个地方:
由上式,要让第二个值较大,只需让 \(h\) 较大的怪物排在前面即可,
综上,按照 \(h\) 递减的方案排序就好了。
最后按照上面的方案模拟即可。
#pragma GCC optimize("O3")
#include<bits/stdc++.h>
using namespace std;
#define SET0(a) memset(a,0,sizeof(a))
#define FOR(i,a,b) for(int i=(a);i<=(b);i++)
#define DWN(i,a,b) for(int i=(a);i>=(b);i--)
#define INF 0x3f3f3f3f
typedef long long ll;
const int N=2e5+5;
struct node{
ll t,h;
bool vis;
}a[N];
ll n,m;
bool cmp(node x,node y){
if(x.h>y.h) return true;
else return false;
}
int main(){
int T; cin>>T;
while(T--){
cin>>n>>m;
FOR(i,1,n) {
cin>>a[i].t>>a[i].h;
a[i].h=min(a[i].h,m); // 真正的恢复能力
a[i].vis=false;
}
ll hp=m;
FOR(i,1,n){
if(a[i].h>=a[i].t){
a[i].vis=true;
hp+=a[i].h-a[i].t;
}
}
sort(a+1,a+1+n,cmp);
bool ok=1;
FOR(i,1,n){
if(a[i].vis) continue;
hp-=a[i].t;
//cerr<<"hp="<<hp<<endl;
if(hp<0){
ok=0;
break;
}
hp+=a[i].h;
}
if(ok) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
return 0;
}
L
我们记 \(1,2,...,j\) 的序列为“龙”, \(j\) 为“龙头”,\(f[j]\) 表示长为 \(j\) 的龙至少删除多少个数才会“消失”(即 \(1,2,...,j\) 的序列不存在)
让“龙”消失的办法有两种:
- 去掉“龙头”
- 去掉除了“龙头”的其他部分
于是可以发现状态转移方程: \(f[j]=min(f[j]+1,f[j-1])\)
\(f[k]\) 即为所求答案。
下面的任务是记录方案:
利用 \(g[]\) 将 \(f[]\) 更新的时候的过程记录下来,然后倒着扫一遍数组。
模拟更新的逆过程删数(打印方案)就可以了。
#pragma GCC optimize("O3")
#include<bits/stdc++.h>
using namespace std;
#define SET0(a) memset(a,0,sizeof(a))
#define FOR(i,a,b) for(int i=(a);i<=(b);i++)
#define DWN(i,a,b) for(int i=(a);i>=(b);i--)
#define INF 0x3f3f3f3f
typedef long long ll;
const int N=1e6+10;
int f[N],g[N];
int n,k;
int a[N];
int main(){
cin>>n>>k;
FOR(i,1,n) cin>>a[i];
FOR(i,1,n){
int v=a[i];
g[i]=f[v];
if(v==1) f[1]++;
else f[v]=min(f[v]+1,f[v-1]);
}
cout<<f[k]<<endl;
int cur=k;
DWN(i,n,1){
int v=a[i];
if(cur==v){
if(g[i]+1==f[v]) cout<<i<<' ';
else cur--;
}
f[v]=g[i];
}
cout<<endl;
return 0;
}