2021-05-23 题解

HZOJ T1(引自Luogu P4316) 绿豆蛙的归宿

Description:

题目背景:

随着新版百度空间的上线,Blog 宠物绿豆蛙完成了它的使命,去寻找它新的归宿。

题目描述:

  给出张 n 个点 m 条边的有向无环图,起点为 1,终点为 n,每条边都有一个长度,并且从起点出发能够到达所有的点,所有的点也都能够到达终点。
  绿豆蛙从起点出发,走向终点。 到达每一个顶点时,如果该节点有 k 条出边,绿豆蛙可以选择任意一条边离开该点,并且走向每条边的概率为 1/k。现在绿豆蛙想知道,从起点走到终点的所经过的路径总长度期望是多少?
输入格式:
  输入的第一行是两个整数,分别代表图的点数 n和边数 m。
  第 2 到第 (m+1) 行,每行有三个整数 u,v,w, 代表存在一条从 u 指向 v 长度为 w 的有向边。
输出格式:
  输出一行一个实数代表答案,四舍五入保留两位小数。
  输入输出样例
   输入 #1:
    4 4
    1 2 1
    1 3 2
    2 3 3
    3 4 4
   输出 #1:
     7.00

基本思路:

  根据E(x)=∑pi*xi可知:
  从起点到终点的期望路径长为每一条边长乘以它被选择的概率的加和,边长已知,要求的是概率。
上代码:

#include<bits/stdc++.h>
#define ll long long
#define rr register 
using namespace std;
const int SIZE=100002;
int n,m;
int indg[SIZE],line[SIZE];//line记录拓扑序,indg是入度
double ans,rate[SIZE];//行进到i的几率
vector<int> to[SIZE];//作者的怪癖,不爱用前向星,不习惯者请手动改正
vector<double> w[SIZE];
int read();
void SORT();//拓扑排序
int main();
{
    n=read();
    m=read();
    for(rr int i=1;i<=m;i++)
    {
        int u=read(),v=read();
        double ww;
        scanf("%lf",&ww);
        indg[v]++;
        to[u]push_back(v);
        w[u].push_back(ww);
    }
    SORT();
    rate[1]=1.0000;
    for(rr int i=1;i<=line[0];i++)    
      for(rr int j=0;j<to[line[i]].size();j++)    
      {
          ans+=w[line[i]][j]*rate[line[i]]/(to[line[i]].size()*1.0000);//这里乘以1.0000是因为size()函数返回值是int
          rate[to[line[i]][j]]+=rate[line[i]]/(to[line[i]].size()*1.0000);
      }
    printf("%.2lf\n",ans);
}
void SORT()
{
   queue<int> q;
   int head;
   for(rr int i=1;i<=n;i++)
   {
      if(!indg[i])
        q.push(i);
   }
   while(!q.empty())
   {
       head=q.front();
       line[++line[0]]=head;
       q.pop();
       for(rr int i=0;i<to[head].size();i++)
       {
           indg[to[head][i]]--;                  
           if(!indg[to[head][i]])
             q.push(to[head][i]);
       }
   }
}
int read()
{
   rr int x_read=0,y_read=1;
   rr char c_read=getchar();
   while(c_read<'0'||c_read>'9')
   {
      if(c_read=='-')
        y_read=-1;
      c_read=getchar();
   }
   while(c_read<='9'&&c_read>='0')
   {
      x_read=(x_read<<3)+(x_read<<1)+(c_read^48);
      c_read=getchar();
   }
   return x_read*y_read;
}

HZOJ T3(引自Luogu P1654) OSU!

Description:

题目描述:

  osu 是一款群众喜闻乐见的休闲软件。
  我们可以把osu的规则简化与改编成以下的样子:
一共有n次操作,每次操作只有成功与失败之分,成功对应1,失败对应0,n次操作对应为1个长度为n的01串。在这个串中连续的 X 个 1 可以贡献 X^3 的分数,这x个1不能被其他连续的1所包含
现在给出n,以及每个操作的成功率,请你输出期望分数,输出四舍五入后保留1位小数

输入格式

  第一行有一个正整数n,表示操作个数。接下去n行每行有一个[0,1]之间的实数,表示每个操作的成功率。
输出格式

  只有一个实数,表示答案。答案四舍五入后保留1位小数。
   输入输出样例:
  输入#1:
    3
    0.5
    0.5
    0.5
  输出 #1:
    6.0

数据范围与提示:

  000分数为0,001分数为1,010分数为1,100分数为1,101分数为2,110分数为8,011分数为8,111分数为27,总和为48,期望为48/8=6.0
N<=100000

基本思路:

  说实话,本人比较笨,才学了概率与期望,刚看道这道题时头比较大,首先,对于每次操作都有一定概率成功或失败,每成功一次,就会在序列上增加一个1,则结果会变为(x+1)^3=(x3+3×x2×1+3×x×1+1)(x为成功前的序列长)
  观察这个式子,可以发现:这个式子里的x3是之前在计算上一个操作时就算出来的,所以这道题可以用概率与期望DP算
  那现在又有一个问题:展开式里同时有二次方与一次方,我们是只维护一次方的值,然后用它二次方还是他们分别维护呢?
        答案是分别维护
我们可以发现:
  如果只维护一次方,转移方程是:
x1[i]=(x1[i-1]+1)×r[i](),x2[i]=x1[i]×x1[i];
  如果同时维护,方程式:
x1[i]=(x1[i-1]+1)×r[i],x2[i]=(x2[i-1]+2×x1[i-1]+1)×r[i];
  如果只维护一次方,那么在使用二次方时,r(概率)会被乘两次,这显然是不对的,所以我们分别维护。
上代码:

#include<bits/stdc++.h>
#define ll long long
#define rr register 
using namespace std;
const int SIZE=100002;
int n;
double x1[SIZE],x2[SIZE];
double c[SIZE],r[SIZE];
int read();
int main();
{
    n=read();
    for(rr int i=1;i<=n;i++)
       scanf("%lf",&r[i]);
    for(rr int i=1;i<=n;i++)
    {
       x1[i]=(x1[i-1]+1)*r[i];
       x2[i]=(x2[i-1]+2*x1[i-1]+1)*r[i];
       c[i]=c[i-1]+(3*x2[i-1]*x1[i-1]+3*x1[i-1]+1)*r[i];
    }
    printf("%.1lf\n",c[n]);
}
int read()
{
   rr int x_read=0,y_read=1;
   rr char c_read=getchar();
   while(c_read<'0'||c_read>'9')
   {
      if(c_read=='-')
        y_read=-1;
      c_read=getchar();
   }
   while(c_read<='9'&&c_read>='0')
   {
      x_read=(x_read<<3)+(x_read<<1)+(c_read^48);
      c_read=getchar();
   }
   return x_read*y_read;
}

HZOJ T4(引自bzoj 1419) Red is good

Description:

  桌面上有R张红牌和B张黑牌,随机打乱顺序后放在桌面上,开始一张一张地翻牌,翻到红牌得到1美元,黑牌则付出1美元。可以随时停止翻牌,在最优策略下平均能得到多少钱。

Input

  一行输入两个数R,B,其值在0到5000之间

Output

  在最优策略下平均能得到多少钱。
Sample Input
5 1
Sample Output
4.166666

注意事项

  输出答案时,小数点后第六位后的全部去掉,不要四舍五入.

基本思路:

  这道题网上好多人写,思路大多雷同,我的思路可能与您的无异,但我不会抄袭,所以请不要喷,谢谢。
这个题在一开始做时我陷入了古典概率学的错误,我推了一个式子,写给大家看下:
x/(x+y)=y/(x+y)×x/(x+y-1)+x/(x+y)×(x-1)/(x+y-1);
(x表示某次抽取后剩下的红牌数,y为黑牌,这个式子左边是红牌第一次抽时被抽到的的概率,右边是第二次,依此类推,推至第n次)
  发现是恒等式
  于是,得出结论:
  无论第几次抽取,红(黑)牌被抽到的概率一定。
  然后就被嘲讽了。。。。。
错误很明显:
  样本空间在变,每变一次,当次的概率是不同的,这个式子其实是由简单随机抽样的全局概率推倒来的,而每次的期望贡献与当次的概率有关,而不能由全局的“平均概率”一概而论,所以不能用
  作者后续又推了几个式子,都是错的,就不赘述了,无奈之下,看了大佬的题解,所以以下的思路是转载的,转载来源请见代码末尾。
  DP的状态数组定义为从c[i][j](表示抽取i张红牌与j张黑牌的期望收益)
  那么方程就是:

f[i][j]=max(0.0,(f[i-1][j]+1)×i/(i+j)+(f[i][j-1]-1)×j/(i+j));

  然后就是循环了。
上代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
int R,B,now;
double f[2][5001];
int main()
{
    scanf("%d%d",&R,&B);
    for (int i=0; i<=R; i++,now^=1,f[now][0]=i)
        for (int j=1; j<=B; j++)
            f[now][j]=max(0*1.0,1.0*i/(i+j)*(f[now^1][j]+1)+1.0*j/(i+j)*(f[now][j-1]-1));
    long long ans=f[R&1][B]*1000000;
    printf("%lf",(double)ans/1000000);
    return 0;
}

  这里直接上的大佬本人的代码,%%大佬。
大佬DaD3zZ的博客.

HZOJ T5(引自bzoj) 守卫者的挑z战

Description:

题目描述:

  打开了黑魔法师Vani的大门,队员们在迷宫般的路上漫无目的地搜寻着关押applepi的监狱的所在地。突然,眼前一道亮光闪过。“我,Nizem,是黑魔法圣殿的守卫者。如果你能通过我的挑战,那么你可以带走黑魔法圣殿的地图……”(废话,划掉) 瞬间,队员们被传送到了一个擂台上,最初身边有一个容量为K的包包。
擂台赛一共有N项挑战,各项挑战依次进行。第i项挑战有一个属性ai,如果ai>=0,表示这次挑战成功后可以再获得一个容量为ai的包包;如果ai=-1,则表示这次挑战成功后可以得到一个大小为1 的地图残片。地图残片必须装在包包里才能带出擂台,包包没有必要全部装满,但是队员们必须把 【获得的所有的】地图残片都带走(没有得到的不用考虑,只需要完成所有N项挑战后背包容量足够容纳地图残片即可),才能拼出完整的地图。并且他们至少要挑战成功L次才能离开擂台。
  队员们一筹莫展之时,善良的守卫者Nizem帮忙预估出了每项挑战成功的概率,其中第i项挑战成功的概率为pi%。现在,请你帮忙预测一下,队员们能够带上他们获得的地图残片离开擂台的概率。
输入格式:
  第一行三个整数N,L,K。
  第二行N个实数,第i个实数pi表示第i项挑战成功的百分比。
  第三行N个整数,第i个整数ai表示第i项挑战的属性值。
输出格式:
  一个整数,表示所求概率,四舍五入保留6 位小数

基本思路:

  可以看出,成功的概率与背包的剩余容量与我们已经完成的任务数有关,那么可以定义状态数组:c[i][j][k],i表示已经做了i个任务,j表示成功了j次,k表示剩余的背包容量。那么有方程:

if(不成功)
c[i+1][j][k]+=c[i][j][k]×(1-rate[i+1]);
if(成功)
c[i+1][j+1][k+f[i+1]]+=c[i][j][k]×rate[i+1];

  然后就是DP了,三层循环,当时打出来时真的吓到作者了,因为时间复杂度看起来就大,不过细算之下,是可以接受的,所以请放心食用。
上代码:

#include<bits/stdc++.h>
#define ll long long
#define rr register
using namespace std;
const int SIZEN=202;
int n,l,k;
int f[SIZEN];
double rate[SIZEN];
double c[SIZEN][SIZEN][SIZEN*2];//一定要开SIZE×2,原因见下面
int read();
void DP(int,int,int,double);
int main()
{
   n=read();
   l=read();
   k=read();
   if(k>n)//一共就n场,大于n的空间没用
     k=n;
   c[0][0][k+200]=1;
   for(rr int i=1;i<=n;i++)
   { 
     scanf("%lf",&rate[i]);
     rate[i]/=100.0000;
   }    
   for(rr int i=1;i<=n;i++)
     f[i]=read();
   for(rr int i=0;i<n;i++)
     for(rr int j=0;j<=i;j++)
       for(rr int m=-i;m<=n;m++)
       {
          DP(i+1,j+1,m+f[i+1],rate[i+1]*c[i][j][m+200]);
          DP(i+1,j,m,(1-rate[i+1])*c[i][j][m+200]);
          //这里为什么m初值为-i以及为什么m要加200解释一下:
          //你有可能面临着前i局全是f[i]==-1而你一直输的情况,
          //那么你的空间就有负值的情况,但下标不接受负值
          //所以,给所有的m统一加上200,200是局数的最大值,
          //可以保证m+200一定为非负值
       }
   double ans=0;
   for(rr int i=0;i<=n;i++)
     for(rr int j=l;j<=n;j++)
       ans+=c[n][j][i+200];
   printf("%.6lf\n",ans);
}
void DP(int x,int y,int z,double w)
{
   if(z>n)
     z=n;
   c[x][y][z+200]+=w;
}
int read()
{
   rr int x_read=0,y_read=1;
   rr char c_read=getchar();
   while(c_read<'0'||c_read>'9')
   {
      if(c_read=='-')
         y_read=-1;
      c_read=getchar();   
   }
   while(c_read<='9'&&c_read>='0')
   {
      x_read=(x_read<<3)+(x_read<<1)+(c_read^48);
      c_read=getchar();
   }
   return x_read*y_read;
}

  作者本人全部的博客都是作者的学习笔记,鉴于作者仍是个比较弱的OIer,难免有错误,如果出错,请您指教。
                  2021.5.23 现役

posted @ 2021-05-23 12:01  Geek_kay  阅读(65)  评论(0编辑  收藏  举报