A - Dice
- 预估难度:
入门
题意
有三个骰子,每个骰子有六个面,每个面上分别写有 \(1,2,3,4,5,6\)。
当同时掷出这些骰子时,掷出的点数之和是否可能为 \(X\)?
思路
能掷出的最小点数为 \(1+1+1=3\),能掷出的最大点数为 \(6+6+6 = 18\)。
代码
void solve()
{int X;cin >> X;if(X >= 3 && X <= 18)cout << "Yes";elsecout << "No";
}
B - 456
- 预估难度:
普及- - 标签:枚举、数学
题意
有三个骰子,每个骰子有六个面。
第 \(i\) 个骰子的第 \(j\) 个面上写有数字 \(A_{i,j}\)。
对于每个骰子,每个面朝上的概率均为 \(\frac{1}{6}\)。
当同时掷出这些骰子时,求掷出的点数中 \(4,5,6\) 各恰好出现一次的概率。
思路
由于每个骰子的每个面都是等概率朝上的,因此我们可以通过枚举法找出每一种可能的掷出情况,然后求出 \(4, 5, 6\) 恰好各出现一次的情况数,记作 \(\text{cnt}\)。
由于总共只有三个骰子,每个骰子都有六种情况,因此总情况数量为 \(6^3 = 216\)。
概率即 \(\dfrac{\text{符合条件的情况数}}{\text{总情况数}} = \dfrac{\text{cnt}}{216}\)。
代码
// 判断 x y z 三个数字是否由 4 5 6 组成
bool check(int x, int y, int z)
{// 排序if(x > y) swap(x, y);if(y > z) swap(y, z);if(x > y) swap(x, y);return x == 4 && y == 5 && z == 6;
}void solve()
{int a[4][7];for(int i = 1; i <= 3; i++)for(int j = 1; j <= 6; j++)cin >> a[i][j];int cnt = 0; // 能够掷出 4 5 6 的事件数量for(int i = 1; i <= 6; i++) // 枚举骰子一的下标for(int j = 1; j <= 6; j++) // 枚举骰子二的下标for(int k = 1; k <= 6; k++) // 枚举骰子三的下标if(check(a[1][i], a[2][j], a[3][k]))cnt++;cout << fixed << setprecision(10) << cnt / 216.0 << "\n";
}
C - Not Adjacent
- 预估难度:
普及- - 标签:枚举
题意
给定一个由字符 a、b、c 组成的字符串 \(S\)。
求 \(S\) 中满足任意两个相邻字符都不相同的非空子串的数量,结果对 \(998244353\) 取模。
即使两个子串作为字符串是相同的,只要它们来自 \(S\) 中的不同位置,就被视为不同的子串。
思路
考虑枚举子串的右端点 \(i\),然后快速计算 \([1, i]\) 范围内有多少个符合条件的左端点。
假设在位置 \(i\) 及之前,上一次出现相邻且相同字符的位置分别为 \(\text{pre}-1\) 以及 \(\text{pre}\)。由于此时右端点固定,因此只要左端点 \(\lt \text{pre}\),子串内就一定会出现相邻且相同的字符,导致不符合题意。因此,符合条件的左端点应在 \([\text{pre}, i]\) 范围内,共 \(i - \text{pre} + 1\) 种。
至于 \(\text{pre}\) 变量,可在枚举右端点 \(i\) 的过程中顺便维护。初始时,由于需要保证左端点 \(\ge 1\),可将 \(\text{pre}\) 变量初始化为 \(1\)。
时间复杂度 \(O(N)\)。
代码
void solve()
{string s;cin >> s;int n = s.size();s = " " + s; // 下标从 1 开始long long ans = 0;int pre = 1;// 记录上一次出现相邻且相同字符的情况时 后一个字符的位置// 即前面最近的一对相邻且相同的子串位置为 pre-1 和 pre// 说明当右端点为 i 时,对应子串的左端点只能在 [pre, i] 内// 由于子串左端点下标不能 < 1,因此 pre 初始置为 1for(int i = 1; i <= n; i++) // 枚举子串右端点{if(s[i] == s[i - 1])pre = i;ans += i - pre + 1;}cout << ans % 998244353;
}
D - Not Adjacent 2
- 预估难度:
普及/提高- - 标签:动态规划
题意
给定一个由字符 a、b、c 组成的字符串 \(S\)。
求 \(S\) 中满足任意两个相邻字符都不相同的非空子序列的数量,结果对 \(998244353\) 取模。
即使两个子序列作为字符串是相同的,只要它们来自 \(S\) 中的不同位置,就被视为不同的子序列。
思路
子序列计数问题,可对每个字符的选或不选两种情况进行决策,因此考虑线性动态规划。
由于题目要求选择的相邻字符不能相同,因此状态中需要有一维用于控制选择的最后一个字符类型。
记 \(\text{dp}[i][j]\) 表示看到第 \(i\) 个位置过,且结尾字符为 \(j\) 的符合条件的子序列数量,其中 \(j = 0,1,2\) 分别对应 a、b、 c。
分类讨论当前最后一个选择的字符 \(j\):
- 当 \(j \ne S_i\),说明此时 \(S_i\) 这个字符必不选,方案数量可直接从前 \(i-1\) 个位置对应的状态直接继承:
- 当 \(j = S_i\),说明 \(S_i\) 这个字符可选可不选:
- 如果要选,则前一个字符不能为 \(S_i\),方案数量即前 \(i-1\) 个位置中结尾字符不为 \(S_i\) 的方案总数再 \(+1\)(表示 \(S_i\) 单独形成一个子串);
- 如果不选,则方案数量即前 \(i-1\) 个位置中结尾字符为 \(S_i\) 的方案数。
时间复杂度 \(O(N \cdot \sigma)\),本题 \(\sigma = 3\)。
代码
typedef long long ll;const ll mod = 998244353;ll dp[300005][3];
// dp[i][j] 表示在 s[1 .. i] 中有多少个符合条件的子序列以 j 结尾void solve()
{string s;cin >> s;int n = s.size();s = " " + s;for(int i = 1; i <= n; i++) // 看到第 i 个位置过{int x = s[i] - 'a'; // 当前字符// 当前字符不选,答案直接继承前 i-1 个位置的对应子序列数量for(int j = 0; j < 3; j++){if(j != x)dp[i][j] = dp[i-1][j];elsedp[i][j] = (dp[i-1][0] + dp[i-1][1] + dp[i-1][2] + 1) % mod;}}cout << (dp[n][0] + dp[n][1] + dp[n][2]) % mod;
}
E - Endless Holidays
- 预估难度:
普及+/提高 - 标签:图论建图、拓扑排序 / 深度优先搜索 / 强连通分量
题意
AtCoder 王国有 \(N\) 个城市,编号为 \(1,2,\dots,N\)。有 \(M\) 条双向道路连接城市对,其中第 \(i\) 条道路连接城市 \(U_i\) 和 \(V_i\)。任意两个城市之间都可以通过若干条道路相互到达。
在 AtCoder 王国,一周有 \(W\) 天。每周按星期 \(1,2,\dots,W\) 的顺序进行,星期 \(W\) 的后一天是星期 \(1\)。
每个城市每星期都有一些特定的节假日。城市 \(i\) 的节假日信息由一个长度为 \(W\) 的字符串 \(S_i\) 给出:如果 \(S_i\) 的第 \(j\) 个字符是 o,则星期 \(j\) 是节假日;如果是 x,则星期 \(j\) 是工作日。
高桥会选择一个他喜欢的城市,并在第 \(1\) 天的中午访问该城市。之后的每个晚上,他反复选择留在当前城市或移动到一个与当前城市直接相连的城市。如果他能一直移动,使得每天中午所在的城市都是节假日,则输出 Yes,否则输出 No。
思路
考虑建图,将”城市 \(i\) - 星期 \(j\)“记作二元组 \((i, j)\),将其哈希为一个 \(1 \sim N \times W\) 范围内的唯一数值,作为图中的点编号。明显这张图可以视作一张分层图,按星期天数进行分层,每层均为 \(N\) 个点,表示星期天数相同的每个城市。
由于只有当第 \(j\) 天城市 \(i\) 是节假日时,高桥才有可能在第 \(j\) 天走到城市 \(i\),那也就相当于此时 \((i, j)\) 所表示的点是可到达的;反之,则标记该点不可到达。
假设第 \(j\) 天高桥位于城市 \(i\),那么第 \(j+1\) 天(如果 \(j=W\) 则记作第 \(1\) 天)高桥有以下两种选择:
- 留在城市 \(i\),这需要建立 \((i, j)\rightarrow (i, j+1)\) 这样一条单向边。
- 走到其它相邻城市 \(t\),这需要建立 \((i, j) \rightarrow (t, j+1)\) 这样一条单项边。
如果高桥有办法在星期一从某个城市 \(x\) 开始,且接下来每天都能够走到一个有节假日的城市,因为城市数量有限但时间无限,明显图中一定会存在一个环,使得从点 \((x, 1)\) 开始能够走到这个环上。
有向图判环,可以使用拓扑排序判断,或是深搜标记判断,或是强连通分量判断均可。
单组数据时间复杂度 \(O((N+M)\cdot W)\)。
代码(拓扑)
int n, m, w, u[100005], v[100005];bool holiday[1000005]; // 记录每个点是否对应是节假日(是否可达)
vector<int> G[1000005]; // 存图
int ind[1000005]; // 记录每个点的入度// 获取城市 i 在星期 j 的对应点编号
int id(int i, int j)
{return (j - 1) * n + i;
}// 建一条 x -> y 的单向边
void addEdge(int x, int y)
{G[x].push_back(y);ind[y]++;
}void solve()
{cin >> n >> m;for(int i = 1; i <= m; i++)cin >> u[i] >> v[i];cin >> w;for(int i = 1; i <= n; i++){string s;cin >> s;for(int j = 1; j <= w; j++)holiday[id(i, j)] = (s[j - 1] == 'o');}// 清空for(int i = 1; i <= n * w; i++){G[i].clear();ind[i] = 0;}// 建图for(int i = 1; i <= m; i++) // 经过边 u[i] <-> v[i]{int x = u[i], y = v[i];for(int j = 1; j <= w; j++){int k = (j == w ? 1 : j + 1); // 第 j 天的后一天if(holiday[id(x, j)] && holiday[id(y, k)]) // 只在这两个点都对应放假时再建边addEdge(id(x, j), id(y, k));if(holiday[id(y, j)] && holiday[id(x, k)])addEdge(id(y, j), id(x, k));}}for(int i = 1; i <= n; i++) // 停在原城市不动for(int j = 1; j <= w; j++){int k = (j == w ? 1 : j + 1); // 第 j 天的后一天if(holiday[id(i, j)] && holiday[id(i, k)])addEdge(id(i, j), id(i, k));}// 找环queue<int> q;for(int i = 1; i <= n * w; i++)if(holiday[i] && ind[i] == 0) // 将所有入度为 0 且可到达的点全部入队q.push(i);while(!q.empty()){int u = q.front();q.pop();for(int &v : G[u])if(--ind[v] == 0)q.push(v);}for(int i = 1; i <= n * w; i++)if(holiday[i] && ind[i] != 0) // 拓扑搜完后,如果还有某个可到达的点 存在入度,说明出现 环{cout << "Yes\n";return;}cout << "No\n";
}
代码(SCC)
int n, m, w, u[100005], v[100005];bool holiday[1000005]; // 记录每个点是否对应是节假日(是否可达)
vector<int> G[1000005]; // 存图// 获取城市 i 在星期 j 的对应点编号
int id(int i, int j)
{return (j - 1) * n + i;
}// 建一条 x -> y 的单向边
void addEdge(int x, int y)
{G[x].push_back(y);
}int dfn[1000005], low[1000005], dfs_clock = 0;
int scc[1000005], scc_cnt = 0, siz[1000005];
// scc[i] 表示 i 所在强连通分量编号
// siz[i] 表示编号为 i 的强连通分量内部点数
stack<int> sk;void tarjan(int u)
{dfn[u] = low[u] = ++dfs_clock;sk.push(u);for(int &v : G[u]){if(!dfn[v]){tarjan(v);low[u] = min(low[u], low[v]);}else if(!scc[v]) // 该点已被搜过但还在栈内low[u] = min(low[u], low[v]);}if(dfn[u] == low[u]){scc_cnt++;int x; // 最后出栈的点do{x = sk.top();sk.pop();scc[x] = scc_cnt;siz[scc_cnt]++;}while(x != u);}
}void solve()
{cin >> n >> m;for(int i = 1; i <= m; i++)cin >> u[i] >> v[i];cin >> w;for(int i = 1; i <= n; i++){string s;cin >> s;for(int j = 1; j <= w; j++)holiday[id(i, j)] = (s[j - 1] == 'o');}// 清空dfs_clock = scc_cnt = 0;for(int i = 1; i <= n * w; i++){G[i].clear();dfn[i] = low[i] = scc[i] = siz[i] = 0;}// 建图for(int i = 1; i <= m; i++) // 经过边 u[i] <-> v[i]{int x = u[i], y = v[i];for(int j = 1; j <= w; j++){int k = (j == w ? 1 : j + 1); // 第 j 天的后一天if(holiday[id(x, j)] && holiday[id(y, k)]) // 只在这两个点都对应放假时再建边addEdge(id(x, j), id(y, k));if(holiday[id(y, j)] && holiday[id(x, k)])addEdge(id(y, j), id(x, k));}}for(int i = 1; i <= n; i++) // 停在原城市不动for(int j = 1; j <= w; j++){int k = (j == w ? 1 : j + 1); // 第 j 天的后一天if(holiday[id(i, j)] && holiday[id(i, k)])addEdge(id(i, j), id(i, k));}// 找环for(int i = 1; i <= n; i++) // 枚举起点城市if(holiday[id(i, 1)] && !dfn[id(i, 1)])tarjan(id(i, 1));for(int i = 1; i <= scc_cnt; i++)if(siz[i] > 1){cout << "Yes\n";return;}cout << "No\n";
}
