考试总结(CE???)

直接开写题解:

(由于T1为暴力模拟,不进行整理)

T2:

扶苏给了你一棵树,这棵树上长满了幼嫩的新叶,我们约定这棵树的根是 1,每个节 点都代表树上的一个叶子。 如果你不知道什么叫树,你可以认为树是一个边数比节点个数少 1 的无向连通图。 我们如果约定节点 u 是树 T 的根,则可以定义一个节点 v 到根的路径为该无向图上 u, v 两个节点之间的简单路径上的节点集合(包括路径的两个端点)。可以证明,这样的简单路 径只有一条。 我们定义节点 x 是节点 y 的祖先(x 不等于 y),当且仅当 x 在 y 到根的路径上。 现在扶苏想在这棵树上选定一个集合,将其称之为幼嫩集合,来比较集合中的节点 哪个最幼嫩。注意到一旦集合中存在两个节点 u, v,使得 u 是 v 的祖先,那么一定 v 要比 u 更幼嫩,因为 v 是在 u 的枝丫上生长出来的,那么这样的集合就是没有意义的。也就是 说,扶苏所选择的集合一定满足要求“对于任意集合中的元素对 (u, v),u 不是 v 的祖 先”。 扶苏其实对这些节点哪个最幼嫩并不感兴趣,也对他能选出多少集合不感兴趣,因 为这些都是为了问你下面的问题而创造出的题目背景。 扶苏给每个节点都定义了一个权值,具体的,我们会给出一个参数 T,规定 i 号节点 的权值为 T i 。 我们定义一个幼嫩集合幼嫩指数为集合内节点的权值和。现在扶苏想请问你,对于 他所有可能选出的集合,这些集合的幼嫩指数之和是多少。 为了避免答案过大,请你输出答案对1000000007取模的结果。

ZAY大佬分层讲了几种方法,分别可以拿到不同层级的分,

1.直接输出答案,只得第一问的分

2.暴搜,复杂度较高,但能拿到30分

3.DP

我们可以把这样的标签:

变成这样的:

考虑将每个节点的状态用子节点表示

1.二叉树状态,每个节点仅有两个分支

先考虑边界条件,即该节点是叶节点时,它没有子树甚至子节点,于是他自己的贡献为自身权值,

再考虑有两个子节点的节点,其上之节点选择方案共有4种,分别是左节点,右节点,自己,左右两节点

再由乘法原理易推知,当前节点的方案总数即为子树根节点方案总数加1之积,就是两子树互相搭配情况下,再算上某子树自身不算的方案总数,这也就是+1的用处

而每个节点的分数就是某棵子树根节点分数乘另一棵子树方案总数,就是自己被选了多少次,由此,可推出答案

2.非二叉树,

每次将同一层的最左端的两棵子树进行如上操作,再将其看作一整体,与下一棵子树进行处理,直到所有子树处理完成

按照以上DP思想处理就可以那全分辣!

代码:

#include <cstdio>

typedef long long int ll;

const int maxn = 1000005;
const int MOD = 1000000007;

template <typename T>
inline void qr(T &x) {
  char ch;
  do { ch = getchar(); } while ((ch > '9') || (ch < '0'));
  do { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } while ((ch >= '0') && (ch <= '9'));
}

int n, T;
int MU[maxn], frog[maxn], gorf[maxn];
bool vis[maxn];

struct Edge {
  int v;
  Edge *nxt;

  Edge(const int _v, Edge *h) : v(_v), nxt(h) {}
};
Edge *hd[maxn];

void dfs(const int u);

int main() {
  freopen("dzy.in", "r", stdin);
  freopen("dzy.out", "w", stdout);
  qr(n); qr(T);
  if (T) {
    for (int i = 1; i <= n; ++i) {
      MU[i] = i;
    }
  } else {
    for (int i = 1; i <= n; ++i) {
      MU[i] = 1;
    }
  }
  for (int i = 1, u, v; i < n; ++i) {
    u = v = 0; qr(u); qr(v);
    hd[u] = new Edge(v, hd[u]);
    hd[v] = new Edge(u, hd[v]);
  }
  dfs(1);
  printf("%d\n", frog[1] % MOD);
  return 0;
}

void dfs(const int u) {
  vis[u] = true;
  for (auto e = hd[u]; e; e = e->nxt) if (!vis[e->v]) {
    int v = e->v;
    dfs(v);
    frog[u] = (frog[u] * (gorf[v] + 1ll) % MOD) + (frog[v] * (gorf[u] + 1ll) % MOD);
    gorf[u] = (gorf[u] + gorf[v] + (1ll * gorf[u] * gorf[v])) % MOD;
  }
  frog[u] = (frog[u] + MU[u]) % MOD;
  ++gorf[u];
}

这么诡异的码风肯定是zay的啊

 T3:

在一个兵荒马乱的年代,有一位画师叫浅溪,非常喜欢画锦鲤。战火烧到了泰 安,他的邻居都惊慌逃命,只有他不舍得池中锦鲤没有离开。这天晚上庭院失火, 池中的锦鲤化妖,用生命护住了画师的平安。

扶苏被画师和锦鲤的故事深深地打动了。为了能让锦鲤和画师继续生活在一起,他 决定回到着火的庭院中灭掉大火。 画师的庭院可以抽象成一个有向图,每个点代表着一个着火的位置。为了量化火势 的大小,扶苏给每个点一个火力值,火力值越大,代表这个点的火势越强。风助火势, 火借风力,对于每一个着火点,都有可能因为大风使得火扩散到其他点。有向图的每条 边 <u,v> 代表大火是从点 u 扩散到点 v 的。需要注意的是一个点可能会扩散到很多 点,也可能是由很多点的大火一起扩散成的。为了不因为灭掉火源让画师发现有人在帮 他灭火,在任意时刻,扶苏不能灭掉任何一个不被任何点所扩散的点的火。一个点的火 被灭掉后,所代表该点的火扩散的所有边将消失。需要说明的是,虽然边消失了,但是 该点扩散到的所有点属性除入度以外都不会改变,更不会消失。 因为穿越的时间有限,扶苏只能灭掉最多 k 个点的火。忙着写题面的扶苏没有时间 算出他最多能扑灭多少火力值,于是他把这个问题交给了你。

便于理解的题面版本: 给你一张有向图,每个点有一个点权。你可以任意选择一个有入度的点,获得它的 点权并把它和它的出边从图上删去。任意时刻不能选择没有入度的点。最多能选择 k 个 点,求最多能获得多少点权。   md我花了10分钟读题你告诉我有简化版???!!! 

老规矩:

1.输出样例 5分(我连输出都没打完就输出答案了,然而建树模板从T2粘下来的,一波窒息操作CE了)

2.暴搜

3.拓扑排序+贪心(有这个思路但时间不够了这题目背景竟该死的甜美

4.也是拓扑,利用深搜原理和强连通分量性质对环进行缩点(tarjan),对于搜索树进行贪心,完成...

我TM刚刚整了套什么?

我们整个下午差不多听题解也是这么个状态:

啊...就是这个这个...那个那个...讲的好啊!

1 minite later

(小声)你听懂了吗?

代码奉上:

#include <cstdio>
#include <algorithm>
#include <functional>
#ifdef ONLINE_JUDGE
#define freopen(a, b, c)
#endif

typedef long long int ll;

namespace IPT {
  const int L = 1000000;
  char buf[L], *front=buf, *end=buf;
  char GetChar() {
    if (front == end) {
      end = buf + fread(front = buf, 1, L, stdin);
      if (front == end) return -1;
    }
    return *(front++);
  }
}

template <typename T>
inline void qr(T &x) {
  char ch = IPT::GetChar(), lst = ' ';
  while ((ch > '9') || (ch < '0')) lst = ch, ch=IPT::GetChar();
  while ((ch >= '0') && (ch <= '9')) x = (x << 1) + (x << 3) + (ch ^ 48), ch = IPT::GetChar();
  if (lst == '-') x = -x;
}

const int maxn = 1000006;

struct Edge {
  int v;
  Edge *nxt;

  Edge(const int _v, Edge *h) : v(_v), nxt(h) {}
};
Edge *hd[maxn];

int n, m, k, vistime, top, scnt;
int MU[maxn], dfn[maxn], low[maxn], stack[maxn], belong[maxn], minv[maxn];
bool instack[maxn], haveind[maxn];

void tarjan(const int u);

int main() {
  freopen("zay.in", "r", stdin);
  freopen("zay.out", "w", stdout);
  qr(n); qr(m); qr(k); MU[0] = 2333;
  for (int i = 1; i <= n; ++i) qr(MU[i]);
  for (int i = 1, u, v; i <= m; ++i) {
    u = v = 0; qr(u); qr(v);
    hd[u] = new Edge(v, hd[u]);
  }
  for (int i = 1; i <= n; ++i) if (!dfn[i]) {
    tarjan(i);
  }
  for (int u = 1; u <= n; ++u) {
    for (auto e = hd[u]; e; e = e->nxt) if (belong[u] != belong[e->v]) {
      haveind[belong[e->v]] = true;
    }
  }
  for (int i = 1; i <= scnt; ++i) if (!haveind[i]) {
    MU[minv[i]] = 0;
  }
  std::nth_element(MU + 1, MU + 1 + k, MU + 1 + n, std::greater<int>());
  int ans = 0;
  for (int i = 1; i <= k; ++i) {
    ans += MU[i];
  }
  printf("%d\n", ans);
  return 0;
}

void tarjan(const int u) {
  dfn[u] = low[u] = ++vistime;
  instack[stack[++top] = u] = true;
  for (auto e = hd[u]; e; e = e->nxt) {
    int v = e->v;
    if (!dfn[v]) {
      tarjan(v);
      low[u] = std::min(low[u], low[v]);
    } else if (instack[v]) {
      low[u] = std::min(low[u], dfn[v]);
    }
  }
  if (dfn[u] == low[u]) {
    int v, &_mv = minv[++scnt];
    do {
      instack[v = stack[top--]] = false;
      belong[v] = scnt;
      if (MU[v] < MU[_mv]) _mv = v;
    } while (v != u);
  }
}

不打using namespace std的神仙...

讲解内容放在这里,看培训完了能不能听懂...

考虑DAG的情况放到普通有向图上会发生什么。

有了子任务 3 的提示,我们可以考虑把整个图缩点,将其变成一个DAG来做。

对于一个DAG,显然可以通过子任务 3 调整顺序的方式使得每个强连通分量的 选择情况除选点个数以外互不影响。故下面只讨论一个强连通分量内部的情况。

一个强连通分量显然可以看作是一棵外向树加上很多边得到的。

一棵外向树的定义:一个外向树的任意一个节点要么为叶节点,要么它与孩子 间的所有边都是由它指向孩子。

一棵外向树显然是一个 DAG 。按照之前对 DAG 上情况的说明,显然我们可以 选择除了根节点以外的任意节点。

因为一个强连通分量内部是互相连通的,于是我们不妨钦定一个点为根。

对于一个没有入度的强连通分量,我们不妨钦定点权最小的点为根。这样显然选择的是最优的。

对于一个有入度的强连通分量,我们不妨钦定那个有入度的点为根。这样在选择到只剩根节点的时候,因为根节点有入度,所以根节点是可以被选择的。于是这个强连通分量可以被全部选择。这显然是最优的。

这样综合上述讨论,有入度的强连通分量可以随便选,没有入度的强连通分量 去掉最小的点权的点。剩下贪心取前 k 个就可以了。

进行一次 tarjan的复杂度是 O(n+m),选前 k 个点可以排序一下。这样总复杂 度 O(m+nlogn),期望得分 35 分。注意到复杂度瓶颈在排序上,考虑我们只需要前 k 大而不需要具体前 k 个之间的大小关系,于是使用 std::nth_element()函数可以将复杂度降至 O(n+m)。期望得分 40 分。注意,输入规模到了 10 7 级别,需要 fread 来实现读入优化。

nth_element()函数是一个理论复杂度为O(n)的排序函数(这么6?)

//update 19/11/22(原谅我曾经英语不好)

nth_element这个函数是找第n大的数,把第n大的数放在n的位置上,比起小的放在其左,反之置于其右,不保证有序,算是qsort的一部分,所以这只是用stl实现了qsort中的部分步骤,然后实现了需要排序才能进行查询的事情(其实某种意义上有种数据结构是能实现不排序查询nth大的数的...)

posted @ 2019-06-22 09:48  _Alex_Mercer  阅读(250)  评论(6编辑  收藏  举报