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

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

时间:2019.3.29

比赛网址

T1:移动“哨兵棋子”

题目描述

给出数轴上的N个棋子,要求移动到连续的N个位置上,只能移动到整点,只能移动两端的“哨兵”棋子,且移动后不能是“哨兵”。求最小移动步数和最大移动步数。N <= 100000

分析

贪心,将“最小”和“最大”分开计算。
贪心策略见下。

最小移动步数

贪心策略:枚举移动之后得到的区间(必须在左右哨兵之间),计算这个区间最多包含多少棋子。若区间最多包含max_cnt个棋子,那么答案就是n - max_cnt

证明:

![](./解题报告 smoj 2019初二创新班(2019.3.24)/1.png)

不妨假设区间的左端点一定是一个棋子,可以发现这是不会对答案产生影响的(超出右哨兵的区间可以看成以右哨兵为结尾)

直接使用双指针(牛吃草)扫描即可

注意这个贪心有一个问题:

![](./解题报告 smoj 2019初二创新班(2019.3.24)/2.png)

当有n - 1个节点连在一起的时候,靠左的哨兵无法移动到剩余的一个空位。在程序里特判一下即可。

代码(最小移动步数)

cur = 1;
pos[n + 1] = kInf;
max_cnt = -1;
for (int i = 1; i <= n; i++) {
  cur = max(cur, i);
  while (pos[cur + 1] - pos[i] + 1 <= n) cur++;
  if (cur - i + 1 == n - 1 && pos[cur] - pos[i] + 1 == n - 1) continue;
  max_cnt = max(max_cnt, cur - i + 1);
}
assert(max_cnt != -1);
printf("%lld\n", n - max_cnt);

最大移动步数

贪心策略:让边缘的两个棋子交替前进,如图。

![](./解题报告 smoj 2019初二创新班(2019.3.24)/3.png)

每次哨兵移动,都会使空白框的长度减去1。讨论一下移动哪个哨兵即可

代码(最大移动步数)

这里choose_left指左边空白的长度(即移动右哨兵),choose_right同理。

for (int i = 1; i <= n; i++) {
  if (i > 1 && i < n) choose_left += pos[i] - pos[i - 1] - 1;
  if (i > 2) choose_right += pos[i] - pos[i - 1] - 1;
}
printf("%lld\n", max(choose_left, choose_right));

总代码

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int kMaxN = 100000 + 10;
const int kInf = 1000000000 + kMaxN + 10;
int n;
int pos[kMaxN];
int choose_left, choose_right;
int cur, max_cnt;
signed main() {
  freopen("2819.in", "r", stdin);
  freopen("2819.out", "w", stdout);
  scanf("%lld", &n);
  for (int i = 1; i <= n; i++) {
    scanf("%lld", &pos[i]);
  }
  sort(pos + 1, pos + n + 1);
  cur = 1;
  pos[n + 1] = kInf;
  max_cnt = -1;
  for (int i = 1; i <= n; i++) {
    cur = max(cur, i);
    while (pos[cur + 1] - pos[i] + 1 <= n) cur++;
    if (cur - i + 1 == n - 1 && pos[cur] - pos[i] + 1 == n - 1) continue;
    max_cnt = max(max_cnt, cur - i + 1);
  }
  assert(max_cnt != -1);
  printf("%lld\n", n - max_cnt);
  for (int i = 1; i <= n; i++) {
    if (i > 1 && i < n) choose_left += pos[i] - pos[i - 1] - 1;
    if (i > 2) choose_right += pos[i] - pos[i - 1] - 1;
  }
  printf("%lld\n", max(choose_left, choose_right));
  return 0;
}

T2:黑白球

题目描述

一个箱子里面有n个黑球m个白球。你每小时都随机从箱子里面抽出两个小球,然后把这两个球都染成黑球,然后再放回去。问需要多少小时才能把所有小球变成黑色小球?输出期望值。

分析

移项期望DP裸题。

\(F(n, m)\)为剩下\(n\)个黑球,\(m\)个白球,将所有白球变成黑球的期望步数。

两次都取出黑球的概率:

\[P_1 = \dfrac {n} {n +m} \times \dfrac {n - 1} {n + m - 1} \]

两种颜色各取出一个的概率:

\[P_2 = \dfrac {n} {n + m} \times \dfrac {m} {n + m - 1} \]

两次都取出白球的概率:

\[P_3 = \dfrac {m} {n + m} \times \dfrac {m - 1} {n + m - 1} \]

DP方程:

\[\begin{align} F(n, m) = &1 + P_1 \times F(n, m) \\ &+ P_2 \times F(n + 1, m - 1) \\ &+ P_3 \times F(n + 2, m - 2) \end{align} \]

移项可得:

\[F(n, m) = \dfrac {1 + P_2 \times F(n + 1, m - 1) + P_3 \times F(n + 2, m - 2)} {1 - P_1} \]

时间复杂度\(O(n)\),空间复杂度\(O(n^2)\)

代码

#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 47 * 4 + 10;
int T;
int n, m;
double arr_prob[kMaxN][kMaxN];
double GetProb(int n, int m) {
  if (m <= 0) {
    return 0;
  } else if (arr_prob[n][m] >= -1) {
    return arr_prob[n][m];
  } else {
    double invtot = 1.0 / (n + m);
    double invtot2 = 1.0 / (n + m - 1);
    double p1 = n * invtot * (n - 1) * invtot2;
    double p2 = n * invtot * m * invtot2
              + m * invtot * n * invtot2;
    double p3 = m * invtot * (m - 1) * invtot2;
    return arr_prob[n][m] = (1 + GetProb(n + 1, m - 1) * p2
                               + GetProb(n + 2, m - 2) * p3) / (1 - p1);
  }
}
int main() {
  freopen("2829.in", "r", stdin);
  freopen("2829.out", "w", stdout);
  scanf("%d", &T);
  while (T--) {
    scanf("%d %d", &n, &m);
    for (int i = 0; i < kMaxN; i++)
      for (int j = 0; j < kMaxN; j++)
        arr_prob[i][j] = -5;
    printf("%lf\n", GetProb(n, m));
  }
  return 0;
}

T3:树与图

题目描述

最近有人发明了一款新游戏。给定具有N个顶点的连通无向图和具有N个节点的树,尝试以下列方式将该树放置在图上:

1、树的每个节点与图的顶点对应。即每个节点对应一个顶点,每个顶点对应一个节点。

2、如果树的两个节点之间存在边,则图中的相应顶点之间也必须存在边。

现在想知道有多少不同的放置方案,答案模1000000007。

数据范围:N <= 14

luogu P3349 [ZJOI2016]小星星

分析

下文为了方便将“树上的点”简称“树点”,图同理。

有一个明显的暴力:枚举每个树点对应的图点。用全排列(Dfs)枚举即可。

让我们思考一下:为什么我们要枚举全排列而不是直接\(N^N\)选择?回答是因为树点要和图点一一对应,已被对应过的图点不能被其他树点再次对应了。

这让我们想到状压DP。引用@shadowice1984的话:

如果你足够熟练的话(参见ZJOI2015地震后的幻想乡&NOIP2017宝藏),你会发现全排列暴力的优化永远都是指向了一个东西——子集dp

状压DP的思路如下,由于笔者没有写过,所以不详细将方程列出:(仍然是引用,引用自@xyz32768

定义状态\(f[i][j][S]\)表示节点\(i\)编号为\(j\)\(i\)的子树内的编号集合为\(S\)的方案数。

但是这样的瓶颈在于枚举子集,复杂度是\(O(n^3\times 3^n)\)的,显然TLE。

下面就让我们来看一种正确的解法

容斥 \(\times\) DP

DP

考虑DP。我们发现由于树中连边的限制,DP需要有一定的顺序(树上父子关系)。设\(F(u)\)为当前考虑树上以\(u\)为根的子树,并将其放在图上的方案数。图上的边也需要考虑,添加一维状态来添加限制。\(\bf{F(u, map)}\)为当前考虑树上以\(\bf{u}\)为根的子树,并将其对应到图上的方案数。其中已经将\(\bf{u}\)与图点\(\bf{mapu}\)进行了对应。

这个方程没有考虑重复对应的问题。我们将在后面的容斥中将这个问题解决,因此方程维数可以不再增加了。

很容易得到转移方程,枚举\(u\)的子树\(v\),并枚举\(v\)所对应的图点即可。方程如下:

\[\Large F(u, mapu) = \displaystyle \prod _ {v \in son(u)} \displaystyle \sum _ {mapv} F(v, mapv) \]

最终答案就是\(\displaystyle \sum _ {mapR} F(root, mapR)\)。使用记忆化搜索可以轻松实现,单次DP的时间复杂度是\(O(N^3)\)

容斥

接下来是考虑重复对应的问题。根据DP的定义,我们发现计算得出的答案是会偏大的。这是因为在一些情况中,两个树点对应到一个图点上;在另一些情况中,三个树点对应到一个图点上……在这些情况中,有一些图点没有被对应。例如:若两个树点对应到一个图点,被对应的图点数就只有\(N - 1\)个。

不妨考虑枚举可能被对应到的图点的集合。由于DP方程的限制,集合中的图点也不是一定能被对应到,集合为\(\{1, 2, 3\}\)时的方案也可能包含\(\{1, 2\}\)。将加多了的减下去,减多了的加回去。直接上容斥就行了。

代码

另外一点,最终的答案不会超过long long的限制,所以也可以输出时再取模。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int kMaxN = 15;
int T;
int n, global_set;
bool graph[kMaxN][kMaxN], tree[kMaxN][kMaxN];
LL arr_dp[kMaxN][kMaxN];
char str[kMaxN];
void ReadGraph(bool graph[kMaxN][kMaxN]) {
  for (int i = 0; i < n; i++) {
    scanf("%s", str);
    for (int j = 0; j < n; j++) {
      graph[i][j] = graph[j][i] = (str[j] == 'Y');
    }
  }
}
void Dfs(int u) { // 确定原树的根为0,将反向边删除
  for (int v = 0; v < n; v++) {
    if (tree[u][v]) {
      tree[v][u] = false;
      Dfs(v);
    }
  }
}
LL Dp(int u, int mapu) {
  if (arr_dp[u][mapu] != -1) {
    return arr_dp[u][mapu];
  } else {
    LL ans = 1;
    for (int v = 0; v < n; v++) {
      if (tree[u][v]) {
        LL sum = 0;
        for (int mapv = 0; mapv < n; mapv++) {
          if (( global_set & (1 << mapv) ) && graph[mapu][mapv]) {
            sum += Dp(v, mapv);
          }
        }
        ans *= sum;
      }
    }
    return arr_dp[u][mapu] = ans;
  }
}
int main() {
  freopen("2830.in", "r", stdin);
  freopen("2830.out", "w", stdout);
  scanf("%d", &T);
  while (T--) {
    memset(graph, false, sizeof(graph));
    memset(tree, false, sizeof(tree));
    scanf("%d", &n);
    ReadGraph(graph);
    ReadGraph(tree);
    Dfs(0);
    LL ans = 0;
    for (global_set = 1; global_set < (1 << n); global_set++) {
      memset(arr_dp, -1, sizeof(arr_dp));
      LL sum = 0;
      int cnt = 0;
      for (int i = 0; i < n; i++) {
        if (global_set & (1 << i)) {
          sum += Dp(0, i);
          cnt++;
        }
      }
      if ((n - cnt) & 1) {
        ans -= sum;
      } else {
        ans += sum;
      }
    }
    printf("%lld\n", ans % 1000000007);
  }
  return 0;
}
posted @ 2019-03-29 19:11  longlongzhu123  阅读(68)  评论(0编辑  收藏  举报