2025牛客寒假算法基础集训营2 出题人题解
非常感谢大家参与本次比赛,祝大家在新的一年里,身体健康,万事如意,心想事成,前程似锦!
由于牛客各个页面使用的 CSS 不同,建议您在我的博客中打开本题解,以获得最优质的阅读体验,点击可跳转。
2025.1.23 20:48 更新
原题解中部分
公式渲染出错,已进行修正。
谢罪部分
本场比赛的《C.字符串外串》与 《D.字符串里串》在书写定义时没有考虑到空串情况,导致题面存在漏洞,且也没有针对这一个边界所构造的数据,在赛后我们已经进行了加强,所带来的不好体验非常抱歉。
此外,在后台收到了大量询问《G.一起铸最好的剑!》、《J.数据时间?》和《K.可以分开吗?》三题题面问题的私信,经确认,几乎都是因为没有认真阅读题面所导致的,希望大家能够认真读题,不要错过题面中的重要信息。
其余的出题趣闻附于文末,再次感谢大家参与!
总览
题号 | 题目名 | 知识点 | Author | 预估难度 |
---|---|---|---|---|
A | 一起奏响历史之音! | 模拟 | WIDA | *100 |
B | 能去你家蹭口饭吃吗 | 排序/二分 | WIDA | *600 |
C | 字符串外串 | 贪心、思维、构造 | WIDA | *1800 |
D | 字符串里串 | 贪心、思维、构造 | WIDA | *1500 |
E | 一起走很长的路! | 思维、前缀和、线段树/ST表 | BingBong | *1700 |
F | 一起找神秘的数! | 位运算、拆位/打表规律 | WIDA | *1000 |
G | 一起铸最好的剑! | 暴力、枚举 | Silencer76 | *900 |
H | 一起画很大的圆! | 几何、构造、数学 | WIDA | *1700 |
I | 一起看很美的日落! | 拆位、树形dp | Bingbong | *2200 |
J | 数据时间? | 模拟、堆 | WIDA | *1300 |
K | 可以分开吗? | dfs/bfs/dsu | BingBong | *1500 |
L | 还会再见吗? | 虚树、换根dp、最短路 | BingBong | *2600 |
M | 那是我们的影子 | 组合数学、分类讨论 | BingBong | *1800 |
A.一起奏响历史之音!
知识点:模拟
打卡。检查给定的七个整数是否仅包含
即可。为了便于书写,我们可以反过来,检查这七个整数是否不为
和
。
时间
;空间
。
参考代码
#include <bits/stdc++.h>
using namespace std;
signed main() {
bool flag = true;
for (int i = 1; i <= 7; i++) {
int x;
cin >> x;
if (x == 4 || x == 7) {
flag = false;
}
}
cout << (flag ? "YES" : "NO") << "\n";
}
B.能去你家蹭口饭吃吗
知识点:排序/二分
解法一:排序
按照题意,对数组从小到大排序。
当
为偶数时,应当输出
;
当
为奇数时,应当输出
;
由于整型除法的运算性质,上面两步可以合并,即直接输出
。
时间
;空间
。
解法二:二分
由于答案具有单调性,枚举当前
值对于数组
是否至少存在一半的值大于
,根据情况更新答案。
时间
;空间
。
参考代码
#include <bits/stdc++.h>
using namespace std;
signed main() {
int n;
cin >> n;
vector<int> in(n + 1);
for (int i = 1; i <= n; i++) {
cin >> in[i];
}
sort(in.begin(), in.end());
cout << in[n / 2 + 1] - 1 << "\n";
}
C.字符串外串
知识点:贪心、思维、构造
本题基于《D.字符串里串》,推荐您在此基础上阅读本题题解。
继续此前的推导,我们容易发现,对于每一个字母,只有其第二次出现和倒数第二次出现的位置才能有效更新答案,因为可爱度的大小只与
重复段的长度相关。
步骤一:构造 最小的字符串
我们不妨使用刚刚的第一种构造方案“
的第一段仅包含一个字符,即
的第一个字母,需要去位置
前寻找;第二段包含
的剩余部分,可以共用这部分”来进行本题的构造。记
取自字符串最后
个字符且第一个字母为
,
的第一个字母位于位置
,构造出的字符串框架如下:
此时,得出第一个无解条件,即
。同时,为了确保不会存在更优的选择,两个
之间不应当存在相同字母、字符串末尾也不应当出现相同的字母,框架进一步明确如下:
此时,达到所能构造的最优情况,其中,省略号部分填入任意字符均不会再影响
的大小,不妨全部统一为
,得到终极构造方案如下。
至此为止,可以得到其中一个无解的条件,即
。
步骤二:构造特定 的字符串
由刚刚的分析得到,所构造的答案只与前后两侧的
强相关,只要这两个部分确定,中间只需要一味的塞入
即可;而前侧的
长度固定为
,后侧的
长度需要小于前侧。
于是,我们联想到取模,只需要不停的塞入长度为
的
即可。
时间
;空间
。
参考代码
#include <bits/stdc++.h>
using namespace std;
signed main() {
int Task = 1;
for (cin >> Task; Task; Task--) {
int n, m;
cin >> n >> m;
if (n == m || n - m > 26) {
cout << "NO\n";
continue;
}
cout << "YES\n";
for (int i = 0; i < n; i++) {
cout << (char)('a' + i % (n - m));
}
cout << "\n";
}
}
D.字符串里串
知识点:贪心、思维、构造
延续题干定义,记选中的子串为
,那么
的最优构造方案为以下两个中的其中一个:
第一段仅包含一个字符,即
的第一个字母,需要去位置
前寻找;第二段包含
的剩余部分,可以共用这部分;
第二段仅包含一个字符,即
的最后一个字母,需要去位置
后寻找;第一段包含
的剩余部分,可以共用这部分;
这样的选择方式最大程度的重复利用了字符,为最佳贪心策略。所以,我们只需要判定是否存在相同的字符,如果是,则更新答案。具体到代码层面上,对于每一种字符,找到其出现的全部位置,依次枚举检查更新答案。虽然依旧存在非常多优化(与 C 的解法相关),但是这样朴素的贪心策略已经可以通过本题,且没有复杂度上的本质差异,所以这里不再赘述其他做法。
需要注意的是,由于不允许选取空串,所以答案不存在
的情况。
时间
;空间
。
参考代码
#include <bits/stdc++.h>
using namespace std;
signed main() {
int n;
string s;
cin >> n >> s;
s = "#" + s;
int ans = 0;
for (int ch = 0; ch < 26; ch++) {
int pre = 0, net = 0;
for (int i = 1; i <= n; i++) {
if (s[i] - 'a' != ch) continue;
if (pre != 0) {
ans = max(ans, n - i + 1);
}
pre = i;
}
for (int i = n; i >= 1; i--) {
if (s[i] - 'a' != ch) continue;
if (net != 0) {
ans = max(ans, i);
}
net = i;
}
}
cout << (ans == 1 ? 0 : ans) << "\n";
}
E. 一起走很长的路!
考虑每个减法是局部的负贡献,而加法是局部的正贡献,若想将每一步的加法变为全局正贡献,只需要把加法操作使用在
,便可以对
起正贡献。
对于每个询问
可以发现本质上是求对于任意的
,那么就可以考虑用前缀和、
表或者线段树维护区间最大值即可,特别的由于
是手推,不需要贡献,所以特判
时不需要操作的情况。
时间
。
参考代码
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
template<typename T>
struct RMQ {
int n;
vector<T>a;
RMQ(vector<T>_a,int _n):a(_a),n(_n){
stmin.resize(n + 5);
for (int i = 0; i <= n; i++) {
stmin[i].resize(21, 0);
}
solve();
}
vector<vector<T>> stmin;
void solve() {
for (int i = 1; i <= n; i++) {
stmin[i][0] = a[i];
}
for (int i = 1; (1 << i) <= n; i++) {
int len = (1 << i);
for (int j = 1; j + len - 1 <= n; j++) {
stmin[j][i] = min(stmin[j][i - 1], stmin[j + (1 << (i - 1))][i - 1]);
}
}
}
T askmin(int l, int r) {
int len = log2(r - l + 1);
return min(stmin[l][len],stmin[r - (1 << len) + 1][len]);
}
};
void solve(){
int n,q;
cin>>n>>q;
vector<i64>a(n+1),s(n+2);
for(int i=1;i<=n;i++){
cin>>a[i];
s[i]=s[i-1]+a[i];
}
vector<i64>cz(n+2);
for(int i=1;i<=n;i++){
cz[i]=s[i-1]-a[i];
}
RMQ<i64> T(cz,n);
while(q--){
int l,r;
cin>>l>>r;
if(l==r){
cout<<0<<"\n";
continue;
}
i64 ts=s[l-1];
cout<<abs(min(0LL,T.askmin(l+1,r)-ts))<<"\n";
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t=1;
while(t--) {
solve();
}
return 0;
}
F. 一起找神秘的数!
知识点:位运算、拆位/打表规律
对于任意两个数
和
,我们可以从二进制的角度来看,对于第
位,记为
和
,有以下四种情况:
当
时:
,
,
。所以
;
当
时:
,
,
。所以
;
当
时:
,
,
。所以
;
当
时:
,
,
。所以
;
综上,我们证明了
。
故题意转化为求
的对数,根据异或性质可得,当且仅当
时等式成立,故答案为区间长度,即
。
时间
。
参考代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
signed main() {
int Task = 1;
for (cin >> Task; Task; Task--) {
i64 l, r;
cin >> l >> r;
cout << (r - l + 1) << "\n";
}
}
G. 一起铸最好的剑!
知识点:暴力、枚举
考虑幂次运算的增长性,最多枚举
次即可,因为
,但是在计算过程中需要使用一个长整型预判断,防止溢出。特别需要注意的是,根据不同人的代码而异,当
时,为边界情况,你可能需要额外特判(在下方示例代码中不需要这样做,全部囊入了循环)。
时间
。
参考代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const i64 inf = 1e18;
signed main() {
int Task = 1;
for (cin >> Task; Task; Task--) {
int n, m;
cin >> n >> m;
i64 Min = 1e18, ans, now = 1;
for (int i = 1; i <= 32; i++) {
if (now >= inf / m) break;
now *= m;
if (abs(n - now) < Min) {
Min = abs(n - now);
ans = i;
}
}
cout << ans << '\n';
}
}
H. 一起画很大的圆!
知识点:几何、构造、数学
首先,我们知道,当三点共线时,可以看作是画出了一个半径为无穷大的圆。所以,当给定的三个点越接近共线,绘制出的圆也就越大。
考虑在给定的矩形边界上选取三个点,使得他们之间最接近共线,显然,我们可以得到两个构造的方向:
长边上选取两个点,短边上选取一个点,且尽可能的靠近长边;
短边上选取两个点,长边上选取一个点,且尽可能的靠近短边;
不妨设长边平行于
轴,短边平行于
轴,我们先讨论第一种构造方向。不妨设在矩形上边界长边上选取两个点
,右边界短边上选取一个点
。
根据正弦定律,我们知道三角形的外接圆半径由
提供。所以,我们应当使得斜边尽可能的大,同时斜边所对的角尽可能的接近
或
。前者很好实现,令长边上的一点位于角落(如图中点
),短边上的点尽可能的靠近角落(如图中点
),此时斜边
取到最大。
在此基础上考虑点
的位置,显然,我们注意到,点
越接近点
,
越接近
。所以我们有结论,长边上一点位于左上角,另一点尽可能与之贴近;短边上一点尽可能贴近右上角,形态如下图:
随后我们考虑第二种构造方式,结论一致,问题在于这两个方案谁画出来的圆更大,答案显然是第一种构造方式更大,依旧通过正弦定理,在长边上选取两点,不仅角更大,斜边也更长,完爆第二种构造方式。
另外需要注意的是,本题卡
long double
。答案非常大,远超想象。
时间
。
参考代码
#include <bits/stdc++.h>
using namespace std;
signed main() {
int T;
cin >> T;
while (T--) {
int a, b, c, d;
cin >> a >> b >> c >> d;
if (b - a < d - c) {
cout << a << " " << c << "\n";
cout << a << " " << c + 1 << "\n";
cout << a + 1 << " " << d << "\n";
} else {
cout << a << " " << d << "\n";
cout << a + 1 << " " << d << "\n";
cout << b << " " << d - 1 << "\n";
}
}
}
I. 一起看很美的日落!
知识点:拆位、树形dp
考虑当前枚举二进制位为第
位。我们用
表示当前结点
的答案总和,
包含结点
且
所贡献的次数 ,
为包含结点
且
所贡献的次数,
表示包含
的连通块个数。
那么每当扫描
的子节点时,由于此时
的父节点还没扫描,所以每次计算的答案都属于满足题意的答案,故可以直接累加。
计算每个结点
时考虑预处理
,
。考虑计算
且
为
的子节点,那么可得:
最后用
加上每个结点当前运算第
位时的答案,由于两两结点运算,最后输出
即可。
进行
次递归和取模常数可能很大,可以考虑优化,多开一个维度的数组来维护二进制的位,当然两种方法都可以通过本题。
时间
。
参考代码
#include <bits/stdc++.h>
using namespace std;
#define i64 long long
const int mod = 1e9 + 7;
const int N = 1e5 + 1;
i64 dp[N];
i64 f[2][30][N];
i64 g[N];
void solve() {
int n;
cin >> n;
vector<i64> a(n + 1);
vector<vector<int>> e(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i], assert(a[i] >= 1 && a[i] <= 1e9);
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
i64 ans = 0;
auto dfs = [&](auto&& self, int u, int fa) -> void {
for (int w = 0; w < 30; w++) {
if ((a[u] >> w) & 1) {
f[1][w][u] = 1;
} else {
f[0][w][u] = 1;
}
}
g[u] = 1;
for (auto v : e[u]) {
if (v == fa) continue;
self(self, v, u);
dp[u] = (dp[u] + dp[v] * g[u] + dp[u] * g[v]) % mod;
for (int w = 0; w < 30; w++) {
dp[u] = (dp[u] + (1LL << w) * f[0][w][u] % mod * f[1][w][v] +
(1LL << w) * f[1][w][u] % mod * f[0][w][v] ) % mod;
f[0][w][u] = (f[0][w][u] + f[0][w][u] * g[v] + f[0][w][v] * g[u]) % mod;
f[1][w][u] = (f[1][w][u] + f[1][w][u] * g[v] + f[1][w][v] * g[u]) % mod;
}
g[u] = (g[u] + g[v] * g[u]) % mod;
}
ans = ans + dp[u];
};
dfs(dfs, 1, 0);
cout << ans * 2 % mod;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
while (t--) solve();
return 0;
}
J. 数据时间?
知识点:模拟、堆
题目读懂了很简单,读入后去重即可。在这里,考察到了一些基础的语法知识,如果您使用的是 C++,那么便捷的格式化输入
scanf
可以帮您节省很多时间;如果您精通字符串切片技术,也可以很快的完成输入。
时间
,空间
。
再次为糟糕的题面描述致歉,非常抱歉。
参考代码
#include <bits/stdc++.h>
using namespace std;
signed main() {
int n, h, m;
cin >> n >> h >> m;
string Date = to_string(h) + "-";
if (m < 10) {
Date += "0" + to_string(m);
}else {
Date += to_string(m);
}
set<string> A, B, C;
while (n--) {
string user, date, time;
cin >> user >> date >> time;
if (date.substr(0, 7) != Date) {
continue;
}
string h = time.substr(0, 2);
if (h == "07" || h == "08" || time == "09:00:00" ||
h == "18" || h == "19" || time == "20:00:00") {
A.insert(user);
}
if (h == "11" || h == "12" || time == "13:00:00") {
B.insert(user);
}
if (h == "22" || h == "23" || h == "00" || time == "01:00:00") {
C.insert(user);
}
}
cout << A.size() << " " << B.size() << " " << C.size() << "\n";
}
K. 可以分开吗?
知识点:dfs/bfs/dsu
考虑最简单的做法,直接对每一个蓝色极大连通块编号,这步可以用 dsu 或者 dfs 实现。
然后把每个
贡献给它所在位置的四个方向存在的蓝色极大联通编号,这里需要注意的是不能对一个蓝色极大连通块重复贡献。然后最后输出所有蓝色极大连通块中被贡献次数的最小值,这一步可以直接
,也可以每次直接比较,都不会超时。
时间
参考代码
#include <bits/stdc++.h>
using namespace std;
#define i64 long long
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
void solve() {
int n, m;
cin >> n >> m;
vector<string> s(n + 1);
for (int i = 1; i <= n; i++) {
cin >> s[i];
s[i] = "%" + s[i];
}
vector<int> g(m * n + 1);
int idx = 0;
vector<vector<bool>> v(n + 1, vector<bool>(m + 1, false));
vector<vector<int>> num(n + 1,
vector<int>(m + 1, 0)); // 每一个红色属于哪个连通块
auto dfs = [&](auto &&self, int x, int y, int id) -> void {
num[x][y] = id;
v[x][y] = true;
for (int i = 0; i < 4; i++) {
int nx = dx[i] + x;
int ny = dy[i] + y;
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && !v[nx][ny] &&
s[nx][ny] == '1') {
self(self, nx, ny, id);
}
}
};
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (!v[i][j] && s[i][j] == '1') {
++idx;
dfs(dfs, i, j, idx);
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i][j] == '0') {
map<int, bool> ex;
for (int k = 0; k < 4; k++) {
int nx = i + dx[k];
int ny = j + dy[k];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && s[nx][ny] == '1' &&
!ex[num[nx][ny]]) {
g[num[nx][ny]]++;
ex[num[nx][ny]] = true;
}
}
}
}
}
sort(g.begin() + 1, g.begin() + idx + 1);
cout << g[1];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
while (t--) solve();
return 0;
}
L. 还会再见吗?
知识点:虚树、换根dp、最短路
我们可以考虑先把所有包含同一种颜色的结点拿出来建立虚树,记当前颜色为
。
对于当前
所对应的虚树上做换根
,把所有结点 从下到
和从上到
的最近关键结点的距离全部找出来,然后便可以更新关键结点的
。
最后每个结点都会有自己的最小答案,对所有已经更新过答案的关键结点跑一遍最短路即可。
由于查询次数较多,建立虚树的过程需要使用效率更高的树剖或者欧拉序。
时间复杂度:
参考代码
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const int INF=1e9;
const int N=5e5+10;
int stk[N];
int ok[N];
int ans[N];
int dp[3][N];
int dp_from[3][N];
int dfn[N],dep[N];
int jump[21][N];
int s[N];
int fa[N];
int heav[N];
int Top[N];
int top=0;
int n,q;
int a,col;
void solve(){
cin>>n>>q;
vector<vector<int>>e(n+1);
vector<int>color;
vector<vector<int>>points_col(n+1);
for(int i=1;i<=n;++i){
ans[i]=1e9;
cin>>a;
map<int,int>cnt;
for(int j=1;j<=a;++j){
cin>>col;
if(cnt[col]>=1){
ans[i]=0;
}else{
points_col[i].push_back(col);
color.push_back(col);
}
cnt[col]++;
}
}
sort(color.begin(),color.end());
color.erase(unique(color.begin(),color.end()),color.end());
auto find=[&](int x)->int {
return lower_bound(color.begin(),color.end(),x)-color.begin()+1;
};
vector<vector<int>>col_point(color.size()+1);
for(int i=1;i<=n;++i){
for(auto col:points_col[i]){
int x=find(col);
col_point[x].push_back(i);
}
}
int u,v;
for(int i=1;i<=n-1;++i){
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
int dfs_idx=0;
auto dfs1=[&](auto &&self,int u,int father)->void {
dfn[u]=++dfs_idx;
dep[u]=dep[father]+1;
fa[u]=father;
s[u]=1;
int k=0;
for(auto v:e[u]){
if(v==father)continue;
self(self,v,u);
s[u]+=s[v];
if(s[v]>k||k==0){
heav[u]=v;
k=s[v];
}
}
if(k==0) heav[u]=u;
};
dfs1(dfs1,1,0);
auto dfs2=[&](auto &&self,int u,int father){
Top[u]=father;
if(heav[u]==0||heav[u]==u) return ;
self(self,heav[u],father);
for(auto v:e[u]){
if(v==fa[u]||v==heav[u]) continue;
self(self,v,v);
}
};
dfs2(dfs2,1,1);
auto get_lca=[&](int u,int v)->int {
while(Top[u]!=Top[v]){
if(dep[Top[u]]<dep[Top[v]]) swap(u,v);
u=fa[Top[u]];
}
return dep[u]<dep[v]?u:v;
};
// 在每个颜色点建立虚树
for(int k=1;k<=color.size();++k){
vector<int>points=col_point[k];
sort(points.begin(),points.end(),[&](int a,int b){
return dfn[a]<dfn[b];
});
stk[top=1]=points[0];
dp[0][points[0]]=dp[1][points[0]]=dp[2][points[0]]=INF;
dp_from[0][points[0]]=dp_from[1][points[0]]=-1;
ok[points[0]]=1;
vector<tuple<int,int,int>>edge;
for(int i=1;i<points.size();++i){
int u=points[i];
ok[u]=1;
dp[0][u]=dp[1][u]=dp[2][u]=INF;
dp_from[0][u]=dp_from[1][u]=-1;
int lca=get_lca(stk[top],u);
dp[0][lca]=dp[1][lca]=dp[2][lca]=INF;
dp_from[0][lca]=dp_from[1][lca]=-1;
if(lca==stk[top]){
stk[++top]=u;
}else{
while(top>1&&dfn[stk[top-1]]>=dfn[lca]){
int dis=dep[stk[top]]-dep[stk[top-1]];
edge.emplace_back(stk[top-1],stk[top],dis);
top--;
}
if(stk[top]!=lca){
int dis=dep[stk[top]]-dep[lca];
edge.emplace_back(lca,stk[top],dis);
stk[top]=lca;
}
stk[++top]=u;
}
}
while(top>1){
int dis=dep[stk[top]]-dep[stk[top-1]];
edge.emplace_back(stk[top-1],stk[top],dis);
top--;
}
// 在该虚树上换根dp
for(auto [u,v,dis]:edge){
if(ok[v]){
// assert(u!=2);
dp[0][v]=0;
dp_from[0][v]=v;
}
if(ok[u]){
dp[0][u]=0;
dp_from[0][u]=u;
}
if(dp[0][v]+dis<=dp[0][u]){
//可以更新第一小
dp[1][u]=dp[0][u];
dp_from[1][u]=dp_from[0][u];
dp[0][u]=dp[0][v]+dis;
dp_from[0][u]=v;
}else if(dp[0][v]+dis<dp[1][u]){
//可以更新第二小
dp[1][u]=dp[0][v]+dis;
dp_from[1][u]=v;
}
}
for(int i=edge.size()-1;i>=0;i--){
auto [u,v,dis]=edge[i];
if(dp_from[0][u]==v){
// 此时更新儿子向上的最短路
dp[2][v]=min(dp[2][u],dp[1][u])+dis;
}else{
dp[2][v]=min(dp[2][u],dp[0][u])+dis;
}
}
for(int i=0;i<points.size();++i){
int u=points[i];
//关键结点
ans[u]=min(ans[u],dp[0][u]+dp[2][u]);
ans[u]=min(ans[u],dp[0][u]+dp[1][u]);
ok[u]=0;//取消关键结点
}
}
priority_queue<pair<int,int>>qq;
for(int i=1;i<=n;++i){
qq.push({-ans[i],i});
}
while(qq.size()){
int u=qq.top().second;
int d=qq.top().first;
qq.pop();
for(auto v:e[u]){
if(ans[v]>ans[u]+1){
ans[v]=ans[u]+1;
qq.push({-ans[v],v});
}
}
}
while(q--){
int u;
cin>>u;
if(ans[u]>=1e9){
cout<<"IMPOSSIBLE!\n";
}else{
cout<<ans[u]<<"\n";
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t=1;
while(t--) {
solve();
}
return 0;
}
M. 那是我们的影子
知识点:组合数学、分类讨论
由于每个
的连续子矩阵都需要满足数独性质,所以可以观察到所有列的标号
在
之后的数字集合是一致的。
首先考虑无解情况(三种情况,以下的列指对
取模后的列编号):
列出现超过
个不同的数字,例如同时存在
;
一个数同时在不同的列;
同一列出现了多个重复数字,例如同时存在
;
然后对于有解的情况,只需要先算出每列的问号个数,记作
。把
之后的列集所缺的数字个数记作
。把剩余
没有出现过的数字个数记为
。最后的答案便是(其中
表示组合数,
表示排列数):
时间复杂度:
参考代码
#include <bits/stdc++.h>
using namespace std;
#define i64 long long
const int mod = 1e9 + 7;
struct Comb {
const int P = 13331;
i64 qmi(i64 a, i64 b) {
i64 ans = 1;
while (b) {
if (b & 1)
ans = ans * a % mod;
a = a * a % mod;
b >>= 1;
}
return ans;
};
void init(int M) {
fac.resize(M + 10, 1);
inv.resize(M + 10, 1);
finv.resize(M + 10, 1);
for (int i = 2; i <= M; i++)
fac[i] = fac[i - 1] * i % mod;
finv[M]=qmi(fac[M],mod-2);
for(int i=M-1;i>=0;i--){
finv[i]=finv[i+1]*(i+1)%mod;
}
inv[1] = qmi(P, mod - 2);
for (int i = 2; i <= M; i++) {
inv[i] = inv[i - 1] * P % mod;
}
};
i64 C(i64 n, i64 m) {
assert(n>=0 && m>=0 && n<=m);
if (n == 0 || m == 0)
return 1ll;
i64 c = fac[m - n] * fac[n] % mod;
return fac[m] *finv[n]% mod * finv[m-n] % mod;
};
i64 A(int n, int m) { return fac[n] * C(n, m) % mod; }
vector<i64> fac, inv,finv;
};
void solve() {
Comb T;
int n;
cin >> n;
T.init(15);
vector<string> s(4);
for (int i = 1; i <= 3; i++) {
cin >> s[i];
assert(s[i].size() == n);
s[i] = '#' + s[i];
}
set<int> v[3];
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= n; j++) {
if (s[i][j] != '?') v[j % 3].insert(s[i][j] - '0');
}
}
// 无解情况
// 1.列出现超过3个数
if (v[0].size() > 3 || v[1].size() > 3 || v[2].size() > 3) {
cout << "0" << endl;
return;
}
// 2.一个数同时在不同的列
for (int i = 1; i <= 9; i++) {
bool ok = false;
for (int j = 0; j < 3; j++) {
if (v[j].find(i) != v[j].end() && ok) {
cout << 0 << endl;
return;
} else if (v[j].find(i) != v[j].end()) {
ok = true;
}
}
}
// 3.同一列出现了多个重复数字
for (int i = 1; i <= n; i++) {
vector<bool> OK(10, false);
for (int j = 1; j <= 3; j++) {
if (s[j][i] == '?') continue;
if (OK[s[j][i] - '0']) {
cout << "0" << endl;
return;
}
OK[s[j][i] - '0'] = true;
}
}
// 每列位置排列位置的个数
vector<i64> f(n + 1);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= 3; j++) {
if (s[j][i] == '?') f[i]++;
f[i] %= mod;
}
}
// 数字分配
vector<i64> ne(3);
int now_use = 0;
for (int j = 0; j < 3; j++) {
ne[j] = 3 - v[j].size();
now_use += (3 - v[j].size());
}
// 第一列缺多少个数字
i64 ans = T.C(ne[0], now_use) * T.C(ne[1], now_use - ne[0]) % mod;
for (int i = 1; i <= n; i++) ans = ans * T.A(f[i], f[i]) % mod;
cout << ans << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
cin >> t;
while (t--) solve();
return 0;
}
趣闻
在今天凌晨其实本场是没有《J. 数据时间?》这个题的,另外还有一个定位 middle 的构造,嘤嘤突然跑过来说觉得前期题太少了,而且第一场难度已经很劝退萌新了,于是在综合考虑之后,我们连夜补出了这个题。就通过人数来看,这个题的定位还算不错,补全了本场前期题没有模拟的问题,补充了 easy 分段的由于时间仓促,导致题面打磨欠佳,所以导致了一大堆人来提问,在这里向大家表示歉意。
在出题阶段,我们的出题人 Bingbong 一直觉得自己的《L.还会再见吗?》是 *2200 水平,自己的《M.那是我们的影子》是 div2.C,这对吗?这不对
是不是该挨打!。
Prepared by WIDA & BingBong & bandiaoz & Silencer76。