牛客暑假多校 2023 第三场
写在前面
比赛地址:https://ac.nowcoder.com/acm/contest/57357。
发生了这种事……气槽她气得吐槽了啊!
以下按个人向难度排序。
A
签到。
除非 否则可以调整为任意数,步数即两数之差。
复制复制// /* By:Luckyblock */ #include <cmath> #include <cstdio> #include <cctype> #include <cstring> #include <algorithm> #define LL long long //============================================================= LL a, b; int n1, n2; char s1[110], s2[110]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } //============================================================= int main() { // freopen("1.txt", "r", stdin); scanf("%s", s1 + 1), scanf("%s", s2 + 1); n1 = strlen(s1 + 1), n2 = strlen(s2 + 1); for (int i = 1; i <= n1; ++ i) { a = 2ll * a + s1[i] - '0'; } for (int i = 1; i <= n2; ++ i) { b = 2ll * b + s2[i] - '0'; } if (a == 0ll && b != 0ll) printf("-1\n"); else printf("%lld\n", b > a ? b - a : a - b); return 0; }
H
发现给定操作不会影响 ,显然只通过给定操作可以构造出所有和为 的数列 。问题变为是否能将 分解为 个质数之和。
显然考虑哥德巴赫猜想:
- :直接判质数。
- :若 为奇数,由于奇数为偶数和奇数之和,则这个偶数只能为 2,可行当且仅当 为质数;若 为偶数则由哥德巴赫猜想可行当且仅当 。
- :由哥德巴赫猜想当且仅当 时可行。
D
手玩一下发现当且仅当矩阵全 0 或全 1 时才满足 。
又发现如果矩阵可以转换为全 0 或全 1,那么所有行与第一行一定是相同或相反的关系;所有列与第一列一定是相同或相反的关系。将所有行/列翻转到与第一行/列相同后,再在另一个方向翻转即可得到全 0/全 1。
检查上述性质是否存在,如果存在再考虑操作步数最少的方案进行翻转即可。
J
发现如果是 DAG 拓扑序即答案,主要问题在如何处理环。
又发现对于一个有向完全图仅需构造正序和反序两个序列即可,则答案一定不超过两个序列。
于是还是考虑拓扑序。先 Tarjan 缩点,然后在缩点后的 DAG 上跑出拓扑序,连通块内部的点顺序任意,然后将拓扑序正序倒序输出即可。
//知识点:缩点 /* By:Luckyblock */ #include <queue> #include <stack> #include <vector> #include <cctype> #include <cstdio> #include <cstring> #include <algorithm> #define LL long long const int kMaxn = 1e6 + 10; const int kMaxm = 2e6 + 10; //============================================================= int n, m; int e_num, head[kMaxn], v[kMaxm], ne[kMaxm]; int d_num, b_num, dfn[kMaxn], low[kMaxn], bel[kMaxn], sz[kMaxn]; int into[kMaxn]; std::vector <int> newv[kMaxn]; std::stack <int> st; std::vector <int> node[kMaxn]; int ansnum = 1; std::vector <int> ans1; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Chkmax(int &fir_, int sec_) { if (sec_ > fir_) fir_ = sec_; } void Chkmin(int &fir_, int sec_) { if (sec_ < fir_) fir_ = sec_; } void AddEdge(int u_, int v_) { v[++ e_num] = v_; ne[e_num] = head[u_]; head[u_] = e_num; } void Tarjan(int u_) { dfn[u_] = low[u_] = ++ d_num; st.push(u_); for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (! dfn[v_]) { Tarjan(v_); Chkmin(low[u_], low[v_]); } else if (! bel[v_]) { Chkmin(low[u_], dfn[v_]); } } if (dfn[u_] == low[u_]) { // ++ b_num; for (int now = 0; now != u_; st.pop()) { now = st.top(); bel[now] = b_num; node[b_num].push_back(now); ++ sz[b_num]; if (sz[b_num] > 1) ansnum = 2; } } } void Topsort() { std::queue <int> q; for (int i = 1; i <= b_num; ++ i) { if (! into[i]) { q.push(i); } } while (! q.empty()) { int u_ = q.front(); q.pop(); for (int i = 0; i < sz[u_]; ++ i) ans1.push_back(node[u_][i]); for (int i = 0, lim = newv[u_].size(); i < lim; ++ i) { int v_ = newv[u_][i]; into[v_] --; if (! into[v_]) { q.push(v_); } } } } //============================================================= int main() { n = read(), m = read(); for (int i = 1; i <= m; ++ i) { int u_ = read(), v_ = read(); AddEdge(u_, v_); } for (int i = 1; i <= n; ++ i) { if (! dfn[i]) Tarjan(i); } for (int u_ = 1; u_ <= n; ++ u_) { for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (bel[u_] == bel[v_]) continue ; newv[bel[u_]].push_back(bel[v_]); into[bel[v_]] ++; } } Topsort(); if (ansnum == 1) { printf("1\n"); for (int i = 0; i < n; ++ i) printf("%d ", ans1[i]); return 0; } printf("2\n"); for (int i = 0; i < n; ++ i) printf("%d ", ans1[i]); printf("\n"); for (int i = n - 1; i >= 0; -- i) printf("%d ", ans1[i]); return 0; }
E
组数据,每组数据给定一 个节点 条边的有向图,边权均为 1,保证从节点 1 出发可以到达其他任意节点。判断以节点 1 为根的所有可能的 DFS 树是否均为以顶点 1 为根的最短路树。
,,。
3S,512MB。
乱搞题把、、、
首先考虑跑出最短路树,也就是 BFS 树对原图进行分层,然后考虑原图中每条边 的两个端点:
- 如果 在 的下一层:这条边本身合法,但这条边可能会对第 3 类边产生影响。
- 如果 和 在同一层:太好了,这条边一定会影响 DFS 树,直接
No
。 - 如果 在 前面的层:如果 是 的支配点,即到达 之前一定会经过 ,那么这条边本身合法;否则可以不到达 而到达 ,进而影响到达 的最短路,即受到了第 1 类边的影响。
然而 DFS 求最短路实在是太假了,非常容易构造出非法的 DFS 树,而且这题数据还水,于是一车随机化和暴力都跑过去了。
赛时写了个啥那是,带着暴力的乱搞。对于 1、2 类边直接判,并且标记第 1 类边的终点;对于第三类边枚举 上所有标记检查该边是否受到了影响。似乎可以证明最多只会跳 次?不太懂。
正解应当是支配树。
支配树板子怎么是黑题啊,跑了。
// /* By:Luckyblock */ #include <queue> #include <cmath> #include <cstdio> #include <cctype> #include <cstring> #include <algorithm> const int kN = 5e5 + 10; const int kM = 5e5 + 10; //============================================================= int n, m; int edgenum, head[kN], v[kM], ne[kM]; bool vis[kN]; int near[kN], topnear[kN]; int dis[kN]; int fa[kN], size[kN], son[kN], top[kN], dep[kN]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Add(int u_, int v_) { v[++ edgenum] = v_; ne[edgenum] = head[u_]; head[u_] = edgenum; } void Init() { n = read(), m = read(); edgenum = 0; for (int i = 1; i <= n; ++ i) { head[i] = fa[i] = 0; size[i] = top[i] = son[i] = dep[i] = 0; dis[i] = 0; vis[i] = 0; near[i] = 0; } for (int i = 1; i <= m; ++ i) { int u_ = read(), v_ = read(); Add(u_, v_); } } void Bfs() { std::queue <int> q; dis[1] = 0; vis[1] = 1; q.push(1); while (!q.empty()) { int u_ = q.front(); q.pop(); for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (!vis[v_]) { dis[v_] = dis[u_] + 1; vis[v_] = 1; q.push(v_); fa[v_] = u_; } } } // for (int i = 1; i <= n; ++ i) printf("%d ", dis[i]); } namespace Cut { void Dfs1(int u_, int fa_) { size[u_] = 1; dep[u_] = dep[fa_] + 1; for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (fa[v_] != u_) continue; Dfs1(v_, u_); if (size[v_] > size[son[u_]]) son[u_] = v_; size[u_] += size[v_]; } } void Dfs2(int u_, int top_) { top[u_] = top_; if (son[u_]) Dfs2(son[u_], top_); for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (fa[v_] != u_) continue; if (v_ == son[u_]) continue ; Dfs2(v_, v_); } } int Lca(int u_, int v_) { for (; top[u_] != top[v_]; u_ = fa[top[u_]]) { if (dep[top[u_]] < dep[top[v_]]) { std::swap(u_, v_); } } return dep[u_] < dep[v_] ? u_ : v_; } } void Dfs3(int u_, int fa_,int near_) { if (near[u_]) { near_ = u_; } else { near[u_] = near_; } for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (fa[v_] != u_) continue; Dfs3(v_, u_, near_); } } //============================================================= int main() { freopen("1.txt", "r", stdin); int T = read(); while (T --) { Init(); Bfs(); Cut::Dfs1(1, 0), Cut::Dfs2(1, 1); int flag = 1; for (int u_ = 1; u_ <= n; ++ u_) { for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i], lca = Cut::Lca(u_, v_); if (v_ == 1) continue; if (lca != u_ && lca != v_) { if (dis[u_] + 1 != dis[v_]) flag = 0; else near[v_] = v_, topnear[v_] = lca; } } } Dfs3(1, 0, 0); for (int u_ = 1; u_ <= n; ++ u_) { for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i], lca = Cut::Lca(u_, v_); if (v_ == 1) continue; if (lca == v_) { int fuck = near[u_]; while (fuck) { if (dep[fuck] > dep[v_] && dep[v_] > dep[topnear[fuck]]) flag = 0; fuck = near[fa[fuck]]; } } else { if (dis[u_] + 1 != dis[v_]) flag = 0; } } } printf("%s\n", flag ? "Yes" : "No"); } return 0; } /* 1 5 5 1 2 1 3 2 5 3 4 4 5 1 4 5 1 2 2 3 3 4 4 2 3 3 1 5 5 1 2 2 3 3 4 4 5 1 5 1 5 6 1 2 1 3 3 4 3 5 2 4 2 5 1 5 6 1 2 2 3 2 5 1 4 4 5 5 4 1 7 8 1 2 2 3 3 4 1 5 5 6 6 5 6 7 3 7 1 5 6 1 2 2 3 2 4 3 5 4 5 5 2 */
B
赌狗啊船正在玩抽牌游戏。游戏规则如下:
- 牌堆可以看做一个由 组成的排列。
- 啊船首先抽出第一张牌。
- 根据最后抽出的牌的点数,啊船会猜测抽到的下一张牌的点数:如果这张牌点数不大于 她会猜测下一张牌点数大于 ,否则她猜测下一张牌点数不大于 。
- 啊船抽出下一张牌并检验自己的猜测是否正确。如果正确则回到步骤 3,如果错误则结束游戏,啊船的得分为当前手中的牌数(包括现在猜错的这张)。
显然牌堆的叠放顺序共有 种。组数据,每组数据给定整数 ,表示啊船对 种牌堆都玩了一次上述游戏,求她的得分总和对 取模的值。
,,,。
3S,256MB。
场上口了但是没时间写了。
DP。
我们称 的数为小数, 的数为大数。发现可以连续抽完一段升序的小数,或者一段降序的大数。
考虑一个可以连续抽出并且还能再抽的牌的前缀,显然它是由升序的小数和降序的大数交替排列构成的;再考虑一个可以连续抽出并且抽完后不能再抽的牌的前缀,显然最后两个位置均为小数或者均为大数,则它是在上述基础上在最后加了和最后一段同类型的一个不合法的数得到的。
于是考虑每次拿出一段某种类型的数接到当前位置之后,钦定这段数有序则可以构造出可以抽完并且还能再抽的前缀,只钦定最后一个位置不有序,就可以构造出可以抽完并且不能再抽的前缀。
具体地,设: 表示满足其中有 个小数,最后一段为小数/大数的抽完还能再抽的 的前缀的种类数;设 表示前缀 为有 个小数,最后一段为小数/大数的抽完不能再抽的牌堆的种类数。
初始化 ,转移时考虑拿出一段与之前一段不同种类的接到当前位置之后。钦定这段数有序来更新 ;只钦定最后一个位置不有序(即将这段中前面的某个数移到最后),后缀 随便排列来更新 ,则有:
答案即:
总时间复杂度 级别。
为了便于书写代码中使用了填表法,并且并没有显式地维护 而是直接将答案累计到了变量中。
// /* By:Luckyblock */ #include <cmath> #include <cstdio> #include <cctype> #include <cstring> #include <algorithm> #define LL long long const int kN = 310; //============================================================= int n; LL p; LL C[kN << 1][kN << 1], fac[kN << 1]; LL ans, f[kN << 1][kN][2]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } void Init() { fac[0] = 1, C[0][0] = 1; for (int i = 1; i <= 2 * n; ++ i) fac[i] = 1ll * fac[i - 1] * i % p; for (int i = 1; i <= 2 * n; ++ i) { C[i][0] = C[i][i] = 1; for (int j = 1; j < i; ++ j) { C[i][j] = C[i - 1][j - 1] + C[i - 1][j]; C[i][j] %= p; } } ans = 0; } void DP() { for (int i = 1; i <= 2 * n; ++ i) { for (int j = 0; j <= n; ++ j) { f[i][j][0] = f[i][j][1] = 0; } } f[0][0][0] = f[0][0][1] = 1; for (int i = 0; i <= 2 * n; ++ i) { for (int j = 0; j <= std::min(i, n); ++ j) { int k = i - j; if (k > n) continue; for (int l = 1; j + l <= n; ++ l) { if (i + l > 2 * n) continue; f[i + l][j + l][0] += f[i][j][1] * C[n - j][l] % p; f[i + l][j + l][0] %= p; ans += f[i][j][1] * C[n - j][l] % p * (l - 1) % p * (i + l) % p * fac[2 * n - i - l] % p; ans %= p; } for (int l = 1; k + l <= n; ++ l) { if (i + l > 2 * n) continue; f[i + l][j][1] += f[i][j][0] * C[n - k][l] % p; f[i + l][j][1] %= p; ans += f[i][j][0] * C[n - k][l] % p * (l - 1) % p * (i + l) % p * fac[2 * n - i - l] % p; ans %= p; } } } ans += 2 * (f[2 * n][n][0] + f[2 * n][n][1]) % p * n % p; ans %= p; } //============================================================= int main() { // freopen("1.txt", "r", stdin); int T = read(); while (T --) { n = read(), p = read(); Init(); DP(); printf("%lld\n", ans); } return 0; }
写在最后
之后可能还回来补个 G 的马拉希。
学到了什么:
- 可以考虑只关注看似值有影响的的元素是否满足某种二元关系。
- 大力乱搞!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现