【UR #6】懒癌

Problem

Description

你绞尽脑汁也没有解开智商锁给的迷题,只见哐地一下门就开了:“您与锁的主人智商一致。”

于是你们窃取了大量内部资料,最后端掉了 \(IIIS\)

但是,虽然 \(IIIS\) 被摧毁了,当地居民仍有大量在星期八休息的,而且看不惯在星期日休息的人,在星期日休息的人同样看不惯在星期八休息的人,于是整个社会秩序被打乱得一塌糊涂。

当地共有 \(2^n - 1\) 个村庄,每个村庄住着 \(n\) 户人家,门牌号分别为 \(1, 2, \dots, n\),每户人家家里养着一条狗。恰逢无药可治的懒癌流行,人人自危。每个村庄都有至少一只狗得了懒癌。一个村庄中,门牌号为 \(i\) 的人家的狗要么得懒癌,要么不得懒癌,一共 \(2^n\) 种情况,再去掉都没得懒癌的情况,一共 \(2^n - 1\) 种。这每种情况都会发生在恰好一个村庄中。

这天来了个善良的人来到每个村庄中,告诉所有人一个爆炸性的新闻:“你们村里至少有一只狗得了懒癌!”

每个村庄中每户人家都不知道自己的狗到底是懒癌还是可爱,但是他能一眼看出某些人家的狗有没有得懒癌。由于这个社会里人与人之间的信任已经崩塌,一个人即使看出别人的狗是否得懒癌也不愿告诉他。

可以用一个 \(n\) 个结点的有向图来描述可见性,\(v\)\(u\) 有一条有向边表示门牌号为 \(v\) 的人家能看出门牌号为 \(u\) 的家里的狗是否得了懒癌,没边则表示看不出。每个人都知道这张有向图。

于是一个残酷的逻辑链条开始启动。对于每个村庄:

第一天,早上每户人家的主人会出门看看别人家的狗,如果一个人能推断出自己家的狗得了懒癌,下午6点整,他就会掏出手枪一枪把自己家的狗毙了。
如果有多个人都在同一天推断出了,那么他们会在下午6点整同时开枪。
每个人都听得到这个村庄里的枪声。如果没有听到枪声,这个村里的人第二天会继续早上出门看狗,推断出自己家狗得了懒癌下午就杀狗。如果还没有听到枪声,第三天也会如此,依次类推。(所以如果一个人听到了枪声那么就不会再开枪杀狗)
作为一个想帮助当地居民调节矛盾的你想要向当地居民展示灾难性的后果,请计算出对于所有前 \(233^n\) 天内有过枪声的村庄:

开枪时间之和。如一个村庄在第 \(k\) 天下午响起枪声,则开枪时间为 \(k\)。(多个人同时开枪只算一次)
死亡的狗的总数。
你只用输出对 \(998244353\)\(7 \times 17 \times 2^{23} + 1\),一个质数)取模后的结果。

输入格式
第一行一个整数 \(n\),含义如前所述。

接下来 \(n\) 行每行 \(n\) 个字符,表示可见性。这 \(n\) 行中的第 \(v\) 行第 \(u\) 个字符为 “1” 表示 \(v\) 能看出 \(u\) 家的狗是否得懒癌,如果字符为 “0” 表示看不出。保证只会出现 “0” 和 “1” 这两种字符,且对于任意一个满足 \(1 \leq v \leq n\)\(v\),第 \(v\) 行第 \(v\) 列为 “0”。

Input Format

第一行一个整数 \(n\),含义如前所述。

接下来 \(n\) 行每行 \(n\) 个字符,表示可见性。这 \(n\) 行中的第 \(v\) 行第 \(u\) 个字符为 “1” 表示 \(v\) 能看出 \(u\) 家的狗是否得懒癌,如果字符为 “0” 表示看不出。保证只会出现 “0” 和 “1” 这两种字符,且对于任意一个满足 \(1 \leq v \leq n\)\(v\),第 \(v\) 行第 \(v\) 列为 “0”。

Output Format

一行输出两个整数分别表示开枪时间和、死亡的狗的总数。

Sample

Input 1

2
01
00

Output 1

5 3

Input 2

2
01
10

Output 2

4 4

Explanation

Explanation for Input 1

门牌号为 1 的人能看见门牌号为 2 的人家里的狗是不是病狗。

共有三个村庄。

1 病了,2 没病。第一天 1 发现 2 没得病,又知道他们中肯定有一个病了,所以第一天下午开枪杀了狗。开枪时间为 \(1\),死了 \(1\) 只狗。
1 没病,2 病了。第一天 1 发现 2 得了病,于是第一天两个人什么也没干。第二天 2 发现前一天 1 没有开枪,所以下午开枪杀了狗。开枪时间为 \(2\),死了 \(1\) 只狗。
1 病了,2 病了。2 还是第二天开枪杀了狗,1 始终没有开枪。开枪时间为 \(2\),死了 \(1\) 只狗。
所以 \(1 + 2 + 2 = 5\)\(1 + 1 + 1 = 3\)

Range

对于 \(20\%\)\(n \leq 8\)

对于 \(20\%\)\(n \leq 20\)

对于 \(20\%\)\(n \leq 100\)

对于 \(10\%\)\(n \leq 3000\) 每户人家都能看出其他每户人家的狗有没有得懒癌

对于 \(30\%\)\(n \leq 3000\)

Algorithm

强联通分量

Mentality

好神的题 \(stO\)

对于一个主人,我们想一下,他在知道这张关系的有向图的情况下,他会怎么考虑?

最暴力的方法:当然是 \(2\) 的幂次枚举所有 他看不到的狗 的得病情况,然后在所有情况里,取可能的开枪时间的最大值。如果到了这个时间还没有人开枪,那就说明自己的狗狗有问题,当天就会把狗 \(bong\) 了。

不过这个太暴力了。

换一种想法,对于一个狗主人,他的思维模式应该是这样子:先假设自己的狗没有生病,然后对于每个看不到的狗,分别假设它们生了病,然后推断看不到的狗的主人又会怎样推断,最后得到最大天数,而其他狗主人也会向这样递归推理。

则我们考虑对于看不到的关系建有向图。

有病的狗为黑点没病的狗为白点,那么每个主人思考的本质就是将自己的点染白,然后将所有后继结点染黑。

这样一来,天数就只与这种染色关系的传递有关。

随后,我们发现,如果这张图的某个点,它能够传到一个强联通分量里,那就永远不会开枪了,因为这样的推理链会陷入一个无限的死循环。

则对于所有能到达强联通分量的点,我们将它们删去,因为它们若为黑点,则永远不会贡献答案。那么最后就会剩下一个漂亮的 \(DAG\)

考虑这种情况下的开枪时间。

对于一个黑点 \(x\),将它能直接到达的所有结点称为后继集合,由于能看到后继集合以外的所有点,那么除了后继集合,它都不需要推测其他结点长什么样子。如果它的后继集合中也存在一个黑点 \(y\),那么由于 \(y\) 能够看到 \(x\) ,根据这个题目最原始的形式(也就是没有有向图限制),则 \(y\) 的一切推论必须建立在 \(x\) 不开枪 的基础上。

换句话说,\(y\) 的开枪时间只可能比 \(x\) 晚。

那么,要计算 \(x\) 的开枪时间,无需管后继集合中的黑点。换句话说,我们只需要计算在 \(x\) 的后继集合中没有黑点,同时 \(x\) 为黑点的最大开枪时间即可。

这样我们需要处理的黑点集合必定满足两两之间互不可达,并且能够看见除了自己的后继集合以外的所有点。那对于任意一个点,它一定能推断出其他点当自己为白点情况下的最小开枪时间,如果到时间还没有开枪,那么当天晚上就会把自己的狗杀掉。

不难得出,这些黑点推断出自己是黑点的时间相同。

考虑使用归纳法。

先看只有一个黑点的情况。

对于当前点 \(x\) ,设 \(x\) 为黑点,图中其他全为白点。

对于出度为零的末梢结点,它能看到所有结点且能看到后继集合,由于一定有黑点,开枪天数为 \(1\)

对于后继结点出度都为零的结点,它会先假设后继结点都为 \(1\) ,由于后继结点互相可见,那么最大开枪时间为结点个数。

则此结点开枪时间为后继结点个数 \(+1\)

依次推论归纳下去,对于每个点,后继集合中没有黑点且自己为黑点的开枪时间为后继集合中的结点个数 \(+1\)

换句话说,它的答案就是自己能到达的点数。

考虑存在 \(2\) 个黑点的情况。对于其中任意一个黑点而言,它能知道如果自己的后继集合里都为白点,另一个黑点的开枪时间。如果到时间还没有开枪,则说明自己的后继集合中有黑点。于是还要花上自己的时间来推出自己为黑点。

这样的话,答案就是两个黑点能到达的点数之和。

推论归纳下去,对于一个互不可达的黑点集合,答案为集合中的点能够到达的点数之和。

所以我们第一问要求的就是:在所有情况中,所有黑点的 后继集合的并 的数量和。

考虑转换成每个点的贡献。设每个点能够被 \(k\) 个结点到达,则它的贡献为 \((2^k-1)*2^{n-k}\)

对于第二问,就是问所有情况中,不能被任何黑点到达的黑点的数量之和。

这种情况下,每个黑点的贡献为 \(2^{n-k}\)

对于求 \(DAG\) 中每个点能被多少个点到达,直接 \(bitset\) 优化,得到 \(O(\frac{nm}{32})\) 复杂度。

完毕。

Code

#include <bits/stdc++.h>
using namespace std;
long long read() {
  long long x = 0, w = 1;
  char ch = getchar();
  while (!isdigit(ch)) w = ch == '-' ? -1 : 1, ch = getchar();
  while (isdigit(ch)) {
    x = (x << 3) + (x << 1) + ch - '0';
    ch = getchar();
  }
  return x * w;
}
const int Max_n = 3005, mod = 998244353;
int n, Res, ans1, ans2;
int cntt, dfn[Max_n], low[Max_n], bel[Max_n], num[Max_n];
int cntp, in[Max_n], top[Max_n];
char P[Max_n][Max_n];
bool bk[Max_n], ins[Max_n];
vector<int> r[Max_n];
bitset<Max_n> s[Max_n];
stack<int> stk;
queue<int> q;
void dfs(int x) {
  dfn[x] = low[x] = ++cntt;
  stk.push(x), ins[x] = 1;
  for (auto to : r[x]) {
    if (!dfn[to])
      dfs(to), low[x] = min(low[x], low[to]);
    else if (ins[to])
      low[x] = min(low[x], dfn[to]);
  }
  if (dfn[x] == low[x]) {
    bel[x] = x;
    while (stk.top() != x) bel[stk.top()] = x, ins[stk.top()] = 0, stk.pop();
    ins[stk.top()] = 0, stk.pop();
  }
}
void del(int x) {
  for (int i = 1; i <= n; i++)
    if (x != i && bk[i] && P[i][x] == '0')
      bk[i] = 0, del(i);
}
int ksm(int a, int b) {
  int res = 1;
  for (; b; b >>= 1, a = 1ll * a * a % mod)
    if (b & 1) res = 1ll * res * a % mod;
  return res;
}
int main() {
#ifndef ONLINE_JUDGE
  freopen("76.in", "r", stdin);
  freopen("76.out", "w", stdout);
#endif
  Res = n = read();
  for (int i = 1; i <= n; i++) {
    scanf("%s", P[i] + 1), s[i].set(i), bk[i] = 1;
    for (int j = 1; j <= n; j++)
      if (P[i][j] == '0' && i != j)
        r[i].push_back(j);
  }
  for (int i = 1; i <= n; i++)
    if (!dfn[i]) dfs(i);
  for (int i = 1; i <= n; i++) num[bel[i]]++;
  for (int i = 1; i <= n; i++)
    if (num[bel[i]] > 1) bk[i] = 0;
  for (int i = 1; i <= n; i++)
    if (!bk[i]) del(i);
  for (int i = 1; i <= n; i++) Res -= !bk[i];
  for (int i = 1; i <= n; i++)
    if (bk[i])
      for (auto to : r[i]) in[to]++;
  for (int i = 1; i <= n; i++)
    if (!in[i] && bk[i]) q.push(i);
  while (!q.empty()) {
    top[++cntp] = q.front();
    for (auto to : r[q.front()])
      if (bk[to] && !(--in[to])) q.push(to);
    q.pop();
  }
  for (int i = 1; i <= cntp; i++) {
    int x = top[i];
    for (auto to : r[x])
      if (bk[to]) s[to] |= s[x];
  }
  for (int i = 1; i <= cntp; i++) {
    int x = top[i];
    int tot = s[x].count();
    ans1 = (ans1 + 1ll * (ksm(2, tot) - 1) * ksm(2, Res - tot) % mod) % mod;
    ans2 = (ans2 + ksm(2, Res - tot)) % mod;
  }
  cout << ans1 << " " << ans2 << endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值