解题报告 smoj 2019初二创新班(2019.3.17)

目录

  • 解题报告 smoj 2019初二创新班(2019.3.17)

    • T1:找玩具

      • 题目描述
      • 题意转化
      • 分析
      • 代码
      • 优化(代码复杂度)
    • T2:闯关游戏
      • 题目描述
      • 分析
      • 代码
    • T3:子数组有主元素
      • 题目描述
      • 分析
      • 代码(\(O(nm\log n)\))
      • 优化
      • 代码(\(O(nm)\))

解题报告 smoj 2019初二创新班(2019.3.17)

时间:2019.3.21

T1:找玩具

题目描述

在游戏开始之前,游戏大师在房间的某些地方隐藏了N个玩具。玩具编号为1到N。您的任务是尽可能多地找到这些玩具。

你没有任何辅助信息就能找到第i个玩具的概率是p[i]%。您每找到一个玩具后,有可能可以得到一些辅助信息,这些辅助信息是告诉您其他某些玩具所在的位置。如果您已经知道玩具的位置,您一定会找到它。

给出二维数组clue[1..N][1..N],其中clue[i][j]=‘Y’表示若找到第i个玩具则会告诉您第j个玩具的具体位置;clue[i][j]=‘N’表示第i个玩具没有第j个玩具位置的辅助信息;

你的任务是计算您在游戏中找到的玩具数量的期望值。

题意转化

这里有一步很重要的转化:将“找到玩具数量的期望值”转化为“每个玩具被找到的概率总和”

这样,我们单独计算每个玩具被找到的概率即可。

另外,还要用到另一个常见套路:求补集

分析

我们把玩具A->根据玩具A找到的玩具当成图上的边。

首先,根据题目我们可以知道:找到一个玩具后,它能走到的所有玩具都能被找到

那么不妨将原图建出来吧!图中的环(强连通分量)中的玩具可以互相找到,因此我们可以缩点,将图变成一个DAG。拓扑排序后进行DP即可。

DP过程:\(玩具A能被找到的概率 = 1 - \displaystyle \prod _ {B能找到A} (玩具B不能被找到的概率)\)

直接按照拓扑顺序求就行了

代码

代码有点丑。时间复杂度:\(O(n +m)\),这里\(n\)、\(m\)分别是点数、边数。

np(not-prob)2就是答案

#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 50 + 10;
int T;
int n;
double p[kMaxN];
char str[kMaxN][kMaxN];
int node_cnt[kMaxN];
struct Graph {
  vector<int> lis[kMaxN];
  bool used[kMaxN][kMaxN];
  void clear() {
    for (int i = 1; i < kMaxN; i++) lis[i].clear();
    memset(used, false, sizeof(used));
  }
  void Add(int u, int v) {
    lis[u].push_back(v);
    used[u][v] = true;
  }
};
Graph G;
int dfn_cnt, color_cnt;
int dfn[kMaxN], low[kMaxN], color[kMaxN];
bool in[kMaxN];
stack<int> S;
double np[kMaxN];
double np2[kMaxN];
void Tarjan(int u) {
  dfn[u] = low[u] = ++dfn_cnt;
  S.push(u);
  in[u] = true;
  for (int i = 0; i < G.lis[u].size(); i++) {
    int v = G.lis[u][i];
    if (!dfn[v]) {
      Tarjan(v);
      low[u] = min(low[u], low[v]);
    } else if (in[v]) {
      low[u] = min(low[u], dfn[v]);
    }
  }
  if (low[u] == dfn[u]) {
    color_cnt++;
    np[color_cnt] = 1.0;
    node_cnt[color_cnt] = 0;
    while (S.top() != u) {
      int v = S.top();
      S.pop();
      color[v] = color_cnt;
      in[v] = false;
      np[color_cnt] *= 1.0 - p[v];
      node_cnt[color_cnt]++;
    }
    S.pop();
    color[u] = color_cnt;
    in[u] = false;
    np[color_cnt] *= 1.0 - p[u];
    node_cnt[color_cnt]++;
    np2[color_cnt] = np[color_cnt];
  }
}
Graph scc;
int cnt_in[kMaxN];
vector<int> vec;
void TopoSort() {
  vec.clear();
  int end = 0;
  for (int i = 1; i <= color_cnt; i++) {
    if (!cnt_in[i]) vec.push_back(i);
  }
  while (end != vec.size()) {
    int u = vec[end++];
    for (int i = 0; i < scc.lis[u].size(); i++) {
      int v = scc.lis[u][i];
      cnt_in[v]--;
      if (!cnt_in[v]) vec.push_back(v);
    }
  }
}
int main() {
  freopen("2823.in", "r", stdin);
  freopen("2823.out", "w", stdout);
  scanf("%d", &T);
  while (T--) {
    G.clear();
    scc.clear();
    memset(dfn, 0, sizeof(dfn));
    memset(low, 0, sizeof(low));
    memset(in, false, sizeof(in));
    memset(color, 0, sizeof(color));
    dfn_cnt = color_cnt = 0;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
      int p1;
      scanf("%d", &p1);
      p[i] = 1.0 * p1 / 100;
    }
    for (int i = 1; i <= n; i++) {
      scanf("%s", str[i] + 1);
      for (int j = 1; j <= n; j++) {
        if (str[i][j] == 'Y') {
          G.Add(i, j);
        }
      }
    }
    for (int i = 1; i <= n; i++)
      if (!dfn[i])
        Tarjan(i);
    memset(cnt_in, 0, sizeof(cnt_in));
    for (int i = 1; i <= n; i++)
      for (int j = 1; j <= n; j++)
        if (str[i][j] == 'Y' && color[i] != color[j]
            && !scc.used[color[i]][color[j]]) {
          scc.Add(color[i], color[j]);
          cnt_in[color[j]]++;
        }
    TopoSort();
    for (int vec_i = 0; vec_i < vec.size(); vec_i++) {
      int u = vec[vec_i];
      for (int i = 0; i < scc.lis[u].size(); i++) {
        int v = scc.lis[u][i];
        np2[v] *= np[u];
      }
    }
    double ans = 0;
    for (int i = 1; i <= color_cnt; i++) {
      ans += (1.0 - np2[i]) * node_cnt[i];
    }
    printf("%lf\n", ans);
  }
  return 0;
}

优化(代码复杂度)

好吧其实并不用这么复杂...

直接DFS判联通就好了嘛OvO

#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 50 + 10;
int T;
int n;
double np[kMaxN]; // not-prob
char str[kMaxN][kMaxN];
bool vis[kMaxN];
bool CanReach(int u, int v) {
  if (vis[u]) {
    return false;
  } else if (u == v) {
    return true;
  } else {
    vis[u] = true;
    for (int i = 1; i <= n; i++) {
      if (str[u][i] == 'Y') {
        if (CanReach(i, v)) return true;
      }
    }
    return false;
  }
}
int main() {
  freopen("2823.in", "r", stdin);
  freopen("2823.out", "w", stdout);
  scanf("%d", &T);
  while (T--) {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
      int a;
      scanf("%d", &a);
      np[i] = 1.0 - 1.0 * a / 100.0;
    }
    for (int i = 1; i <= n; i++) {
      scanf("%s", str[i] + 1);
    }
    double ans = 0;
    for (int i = 1; i <= n; i++) {
      double prob = 1;
      for (int j = 1; j <= n; j++) {
        memset(vis, false, sizeof(vis));
        if (CanReach(j, i)) prob *= np[j];
      }
      ans += 1 - prob;
    }
    printf("%lf\n", ans);
  }
  return 0;
}

T2:闯关游戏

题目描述

艾伦正在闯关,总共有\(N\)个关卡,编号从\(1\)到\(N\)。

每当艾伦尝试闯第\(i\)关时,他要么顺利通过,要么“挂掉”。他完成该关卡的概率总是\(\dfrac{prob[i]}{1000}\)(他总是尽力完成每个关卡)。在关卡\(i\)的末尾有一个宝箱,其中包含价值 \(value[i]\)单位的金币。当艾伦完成关卡时,他从拿起该关卡的金币。

艾伦从第1级关卡开始,艾伦没有金币。对于每个有效的i,每当Allen完成关卡i时,他就会继续进行关卡i + 1,一旦完成第N个关卡,游戏就会结束。

每当艾伦“挂掉”时,下面4件事情会按顺序发生:

  1. 除了艾伦携带的金币,其他金币都被从游戏中移除。
  2. 艾伦目前所携带的所有金币都“坠落”,“坠落”的地点就是艾伦“挂掉”的那个关卡的开头处。一旦艾伦以后再次达到这个关卡,即使在尝试闯该关卡之前,他也能够再次“捡起”上次“坠落”到这个地方的金币。(请注意,如果他在到达这个关卡之前再次“挂掉”,这些金币将永远消失。)
  3. 所有箱子都添加了新的金币,各个箱子金币的量就是它们最初所含的量(相当于初始化各个箱子的金币量)。
  4. 艾伦回到了第1级的开头,且他没有携带金币。艾伦有无限条“生命”,“挂掉”后可以重新开始游戏。

通过上面的规则可以发现,艾伦“挂掉”后,最多只有一堆金币不在宝箱中,这一堆金币的位置就是艾伦最近“挂掉”所在的关卡的开头处。

分析

可以发现,每次艾伦挂掉时只有两种情况:

  1. 他在挂掉之前拿到了上次坠落的金币!
  2. 太非了没有拿到上次坠落的金币。上次的金币消失。

由此可见,坠落的金币数量要么不断叠加,位置不断往后要么直接消失

假设他不停闯关,最终通关后统计数据如下:

其中横轴表示闯关的次数,纵轴表示该次闯关到达的关数。用橙色标出了终点的位置,最后一次闯关直接通关。

可以发现,只有红框框出的蓝色部分(高度单调不下降),才能对答案产生贡献,且可以把蓝色部分的“面积”看做是答案。

考虑DP。设\(F(i)\)表示最后一次闯关通过第\(i\)关后,手上金币数量的期望。
也就是当最后一条蓝柱的高度为\(i\)时,前面所有蓝柱的期望总“面积”

设\(G(i)\)为一次性通过前\(i\)关,并在第\(i+1\)关前挂掉的概率。即\(G(i) = (1 - prob _ {i + 1}) \times \displaystyle \prod _ {1 \le j \le i} ^ i prob_j\)

转移很显然:\(F(i) = \displaystyle \sum _ {1 \le j \le i} value_j + \displaystyle \sum _ {1 \le j \le i} F(j)G(j)\)

等式两边都有\(F(i)\)一项,移一下项即可。
\[
F(i) = \dfrac
{ \displaystyle \sum _ {1 \le j \le i} value_j + \displaystyle \sum _ {1 \le j \le i - 1} F(j)G(j) }
{1 - G(i)}
\]

代码

也可以把\(F(j)G(j)\)理解为在第\(j\)关后期望掉落的金币数量。代码很短。

#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1000 + 10;
int T;
int n;
double prob[kMaxN], g[kMaxN], f[kMaxN];
long long value[kMaxN];
int main() {
  freopen("2827.in", "r", stdin);
  freopen("2827.out", "w", stdout);
  scanf("%d", &T);
  while (T--) {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
      int p;
      scanf("%d", &p);
      prob[i] = 1.0 * p / 1000;
    }
    prob[n + 1] = 1.0;
    for (int i = 1; i <= n; i++) {
      scanf("%lld", &value[i]);
    }
    double pi = 1;
    for (int i = 1; i <= n; i++) {
      pi *= prob[i];
      g[i] = pi * (1 - prob[i + 1]);
    }
    for (int i = 1; i <= n; i++) {
      f[i] = 0;
      for (int j = 1; j <= i - 1; j++) {
        f[i] += f[j] * g[j] + value[j];
      }
      f[i] += value[i];
      f[i] /= 1 - g[i];
    }
    printf("%lf\n", f[n]);
  }
  return 0;
}

T3:子数组有主元素

题目描述

给出一个长为\(N\)的数组(\(N \le 10^5, a[i] \le 50\)),求该数组中含有主元素的子序列个数。数据保证随机。

分析

由于数字的范围很小,所以可以枚举主元素,并统计答案。(主元素对答案的贡献是不会有重复的)

设当前枚举的数字是\(m\),可以将数组中等于\(m\)的位置看成\(+1\),其他看成\(-1\),并做一遍前缀和。

m = 0的情况:
原数组:[0, 0, 1, 2, 0, 0, 0, 1, 2, 1, 0, 1]
     +  +  -  -  +  +  +  -  -  -  +  -
前缀和:[1, 2, 1, 0, 1, 2, 3, 2, 1, 0, 1, 0]

可以发现前缀和其实就是前缀m的个数非m的个数的差值。而区间有主元素的条件就是这段区间的部分和>0。

枚举区间开头\(i\),对答案的贡献就是后缀\(sum[j] >sum[i - 1]\)的\(j\)的个数。

可以用主席树或树状数组解决。

代码(\(O(nm\log n)\))

代码中为了方便,前缀和变成了后缀和,枚举开头变成枚举结尾。其他部分一样。

#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 100000 + 10;
#define lowbit(X) ((X) & -(X))
struct FenwickTree {
  int n;
  int tree[kMaxN << 1]; // 有偏差值,所以要开两倍
  void Clear() {
    for (int i = 1; i <= n * 2; i++) tree[i] = 0;
  }
  void Update(int pos) {
    pos += n;
    for (; pos <= n * 2; pos += lowbit(pos)) tree[pos]++;
  }
  int Query(int pos) {
    pos += n;
    int ans = 0;
    for (; pos; pos -= lowbit(pos)) ans += tree[pos];
    return ans;
  }
};
int n, seed, m;
FenwickTree T;
int a[kMaxN];
int suf_sum[kMaxN];
int GenRand() {
  int ans = (seed >> 16) % m;
  seed = (1ll * seed * 1103515245ll + 12345) % (1ll << 31);
  return ans;
}
int main() {
  freopen("2828.in", "r", stdin);
  freopen("2828.out", "w", stdout);
  scanf("%d %d %d", &n, &seed, &m);
  T.n = n + 1;
  for (int i = 1; i <= n; i++) {
    a[i] = GenRand();
  }
  long long ans = 0;
  // 枚举主元素
  for (int num = 0; num < m; num++) {
    T.Clear();
    for (int i = n; i >= 1; i--) {
      suf_sum[i] = suf_sum[i + 1] + (a[i] == num ? +1 : -1);
    }
    for (int i = 1; i <= n; i++) {
      T.Update(suf_sum[i]);
      ans += i - T.Query(suf_sum[i + 1]);
    }
  }
  printf("%lld\n", ans);
  return 0;
}

优化

思考树状数组的本质:

维护一个数据结构,支持:

  1. 插入一个数
  2. 查询一个数的排名

每次插入/查询的值相差不超过2

我们发现每次插入/查询的值相差不超过2,也就是说可以利用这个性质,减小数据结构的复杂度。

将树状数组改成类似莫队的指针跳跃,直接将复杂度降到\(O(nm)\)

代码(\(O(nm)\))

#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 100000 + 10;
struct DataStructure {
  int n;
  int cnt[kMaxN << 1];
  int cur, rank;
  void Clear() {
    for (int i = 1; i <= n * 2; i++) cnt[i] = 0;
    cur = rank = 0;
  }
  void Insert(int val) {
    val += n;
    while (cur < val) rank += cnt[++cur];
    while (cur > val) rank -= cnt[cur--];
    cnt[cur]++;
    rank++;
  }
  int Query(int val) {
    val += n;
    while (cur < val) rank += cnt[++cur];
    while (cur > val) rank -= cnt[cur--];
    return rank;
  }
};
int n, seed, m;
DataStructure T;
int a[kMaxN];
int suf_sum[kMaxN];
int GenRand() {
  int ans = (seed >> 16) % m;
  seed = (1ll * seed * 1103515245ll + 12345) % (1ll << 31);
  return ans;
}
int main() {
  freopen("2828.in", "r", stdin);
  freopen("2828.out", "w", stdout);
  scanf("%d %d %d", &n, &seed, &m);
  T.n = n + 1;
  for (int i = 1; i <= n; i++) {
    a[i] = GenRand();
  }
  long long ans = 0;
  // 枚举主元素
  for (int num = 0; num < m; num++) {
    T.Clear();
    for (int i = n; i >= 1; i--) {
      suf_sum[i] = suf_sum[i + 1] + (a[i] == num ? +1 : -1);
    }
    for (int i = 1; i <= n; i++) {
      T.Insert(suf_sum[i]);
      ans += i - T.Query(suf_sum[i + 1]);
    }
  }
  printf("%lld\n", ans);
  return 0;
}

原文地址:https://www.cnblogs.com/longlongzhu123/p/10615069.html

时间: 2024-11-08 09:52:34

解题报告 smoj 2019初二创新班(2019.3.17)的相关文章

解题报告 smoj 2019初二创新班(2019.3.31)

目录 T1:单人游戏 题目描述 分析 证明:游戏必定存在环 证明:以最短路径到达环必定最优 证明:移动时不可能越过环的结尾 DP实现 代码 T2:赚金币 题目描述 分析 代码 T3:抽奖 题目描述 分析 代码 时间:2019.4.5 比赛网址 T1:单人游戏 题目描述 棋盘由N个格子排成一行,从左到右编号为1到N,每个格子都有一个相关的价值. 最初,棋子位于第1个格子上,当前方向是向右的. 在每个回合中,棋子在当前方向上行走零步或多步,每一步就是走一个格子.然后在下一回合中,棋子的方向反转. 一

2019模拟赛09场解题报告

目录 2019模拟赛09场解题报告 目录la~~ 题一:瞬间移动 题二:食物订购 题三:马蹄印 题四:景观美化 2019模拟赛09场解题报告 标签(空格分隔): 解题报告 Forever_chen 2019.8.20 目录la~~ 题一:瞬间移动 [题面] 有一天,暮光闪闪突然对如何将一个整数序列a1,a2,...,an排序为一个不下降序列起了兴趣.身为一只年轻独角兽的她,只能进行一种叫做"单元转换"(unit shift)的操作.换句话说,她可以将序列的最后一个元素移动到它的起始位置

解题报告-2019.12.16

解题报告-2019.12 题目:6-3[拓展编程题_课后练习3][P215 习题8-三-4] 报数 (20分) 题目详情: 报数游戏是这样的:有n个人围成一圈,按顺序从1到n编好号.从第一个人开始报数,报到m(<n)的人退出圈子:下一个人从1开始报数,报到m的人退出圈子.如此下去,直到留下最后一个人. 本题要求编写函数,给出每个人的退出顺序编号. 函数接口定义:void CountOff( int n, int m, int out[] ); 其中n是初始人数:m是游戏规定的退出位次(保证为小于

2020-3-14 acm训练联盟周赛Preliminaries for Benelux Algorithm Programming Contest 2019 解题报告+补题报告

2020-3-15比赛解题报告+2020-3-8—2020-3-15的补题报告 2020-3-15比赛题解 训练联盟周赛Preliminaries for Benelux Algorithm Programming Contest 2019  A建筑(模拟) 耗时:3ms 244KB 建筑 你哥哥在最近的建筑问题突破大会上获得了一个奖项 并获得了千载难逢的重新设计城市中心的机会 他最喜欢的城市奈梅根.由于城市布局中最引人注目的部分是天际线, 你的兄弟已经开始为他想要北方和东方的天际线画一些想法

夏令营提高班上午上机测试 Day 4 解题报告

我要是没记错的话,今天的题难度算挺适中的. *标程来自高天宇哥哥 T1:小G的字符串 题目描述 有一天,小 L 给小 G 出了这样一道题:生成一个长度为 n 的.全由小写英文字母构成的字符串,只能使用 k 种字母.要求满足: 字符串中相邻的两个字母不能相同. 必须出现恰好 k 种不同的字母. 这样的合法字符串可能有很多,小 L 让小 G 输出字典序最小的那个. 小 G 太笨啦,不会做这道题,希望你帮帮他. 输入格式 输入文件只有两个数字 n; k,含义如题. 输出格式 输出文件共一行,输出合法的

圆锥曲线:椭圆小题解题报告

圆锥曲线:椭圆小题解题报告 注意事项: 由于本人水平有限,部分题目解题方法可能非最优解,如有更好方法欢迎在评论区指正. 部分题目讲解可能过于口语化,导致并不符合官方(人教版教材)的要求,请各位在考试中不要学习,使用正确的,符合要求的用语. 本文中可能存在错别字,望发现者在评论区指正. 本篇博客是为记录本人在完成学校作业的过程中遇到的问题,同时给部分同学作为解题参考用. 本篇博客中绘制图像的工具是geogebra. 1~10题: 1 题目: 已知F~1~,F~2~是椭圆\(x^2/4+y^2/3=

POJ 1001 解题报告 高精度大整数乘法模版

题目是POJ1001 Exponentiation  虽然是小数的幂 最终还是转化为大整数的乘法 这道题要考虑的边界情况比较多 做这道题的时候,我分析了 网上的两个解题报告,发现都有错误,说明OJ对于错误的判断还不够严厉. 对边界情况的讨论其实应该是思维严密的表现,当然这并不能表明我写的一点错误都没有,只是多多分析一下还是很有好处的. #include <iostream> #include <fstream> #include <string> #include &l

解题报告 之 POJ3057 Evacuation

解题报告 之 POJ3057 Evacuation Description Fires can be disastrous, especially when a fire breaks out in a room that is completely filled with people. Rooms usually have a couple of exits and emergency exits, but with everyone rushing out at the same time

hdu 1541 Stars 解题报告

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1541 题目意思:有 N 颗星星,每颗星星都有各自的等级.给出每颗星星的坐标(x, y),它的等级由所有比它低层(或者同层)的或者在它左手边的星星数决定.计算出每个等级(0 ~ n-1)的星星各有多少颗. 我只能说,题目换了一下就不会变通了,泪~~~~ 星星的分布是不是很像树状数组呢~~~没错,就是树状数组题来滴! 按照题目输入,当前星星与后面的星星没有关系.所以只要把 x 之前的横坐标加起来就可以了