The 2024 CCPC National Invitational Contest (Northeast), The 18th Northeast Collegiate Programming Contest
写在前面
比赛地址:https://codeforces.com/gym/105173
以下按个人难度向排序。
就俩人刚开学处于唐氏状态于是开把省赛,呃呃然而还是唐多亏 dztlb 大神爆切两道计数还不算烂。
J
签到。
唉感觉读研究生好可怕感觉还不如直接去打工不想打了就跑路。
Code by dztlb:
复制复制#include<bits/stdc++.h> using namespace std; #define int long long const int lim=1e18; const int N=2e5+5; inline int read(){ int x=0,f=1; char s; while((s=getchar())<'0'||s>'9') if(s=='-') f=-1; while(s>='0'&&s<='9') x=(x*10)+(s^'0'),s=getchar(); return x*f; } int T; signed main(){ puts("39.20"); return 0; }
D
签到,博弈,
实际诈骗题,手玩下发现后手获胜的情况根本不存在,于是直接 lose
。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long //============================================================= //============================================================= //============================================================= int main() { //freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); int T; std::cin >> T; while (T --) { int n; std::cin >> n; std::cout << "lose\n"; } return 0; }
A
签到,枚举。
乘方后再开方是无贡献的,则操作顺序一定是先一直开方再一直乘方。
于是直接枚举开方次数,再大力乘方直到第一个已经出现过的数或者大于上限 ,则可以保证之后的乘方的值一定不会重复出现。
偷懒写个 map,总时间复杂度 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const LL kInf = 1e9; //============================================================= LL ans; std::map<LL, bool> yes; //============================================================= void check(int remain_, LL t_) { ++ ans; yes[t_] = 1; int flag = 0; while (remain_ && t_ <= kInf) { t_ = t_ * t_; if (yes.count(t_)) { flag = 1; break; } yes[t_] = 1; -- remain_; ++ ans; } if (!flag) ans += remain_; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); LL x, k; std::cin >> x >> k; if (x == 1) { std::cout << 1 << "\n"; return 0; } ans = 1 + k; yes[x] = 1; for (int i = 1; i <= k; ++ i) { LL t = sqrt(x); if (t == 1) { ++ ans; break; } check(k - i, t); x = t; } std::cout << ans << "\n"; return 0; } /* 9 10 5 2 */
E
签到,数学,枚举。
设给定字符串中 1 的数量为 ,记 表示某个正整数中 1 的个数,则有下式成立:
把 拆掉然后移项,则有:
此时等式左右两部分是独立的,于是考虑预处理右边,并枚举左边。因为 ,则 ,可行的 的数量级为 级别,于是可以考虑直接大力枚举 计算 ,并检查对应的 的最小值即可。
总时间复杂度 级别。
妈的赛时没特判 0 挂了四发太唐乐。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e6 + 10; //============================================================= int n, k, m, all, ans; std::string s; int val[kN]; //============================================================= int count(int x_) { int ret = 0; while (x_) { if (x_ & 1) ++ ret; x_ >>= 1; } return ret; } std::string get(int x_) { std::string t; for (int i = 1; i <= k; ++ i) { if (x_ & 1) t.push_back('1'); else t.push_back('0'); x_ >>= 1; } std::reverse(t.begin(), t.end()); return t; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); for (int i = 0; i <= 2e6; ++ i) val[i] = -1; for (int i = 0; i <= 2e6; ++ i) { int c = count(i); if (val[i - c] == -1) val[i - c] = i; } int T; std::cin >> T; while (T --) { std::cin >> n >> k; std::cin >> s; s = "$" + s; m = 0, all = (1 << k), ans = kN; for (int i = 1; i <= n; ++ i) m += (s[i] == '1'); for (int i = 0; i * all <= m; ++ i) { if (0 <= val[m - i * all] && val[m - i * all] < all) { ans = std::min(ans, val[m - i * all]); } } if (ans == kN) std::cout << "None\n"; else std::cout << get(ans) << "\n"; } return 0; } /* 1 2 1 0 */
M
枚举,计算几何
实际大力枚举题。
发现房子的上下两部分实际上是独立的,仅需保证这两部分分别合法,且在天花板不同的两侧即可。判断直角使用点乘,判断点在直线的哪一侧使用叉乘即可。
于是考虑 地枚举天花板和房顶并预处理对于每个天花板,两侧合法的屋顶数量有多少。然后再 地枚举天花板和房子下部分矩形的另一个点,并计算第四个点检查是否存在,再直接检查天花板另一侧的屋顶数量即可。
发现仅比较距离和算点乘叉乘的符号因此可以全程用 LL。偷懒写个 map,总时间复杂度 级别。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long #define pr std::pair #define mp std::make_pair const int kN = 310; //============================================================= int n, x[kN], y[kN]; struct Point { LL x, y; } a[kN]; std::map <pr <LL, LL>, bool> node; int roof[kN][kN][2]; //============================================================= LL distance2(LL x1_, LL y1_, LL x2_, LL y2_) { return 1ll * (x1_ - x2_) * (x1_ - x2_) + (y1_ - y2_) * (y1_ - y2_); } LL dotProduct(LL x1_, LL y1_, LL x2_, LL y2_) { return 1ll * x1_ * x2_ + y1_ * y2_; } LL crossProduct(LL x1_, LL y1_, LL x2_, LL y2_) { return 1ll * x1_ * y2_ - x2_ * y1_; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); std::cin >> n; for (int i = 1; i <= n; ++ i) { std::cin >> x[i] >> y[i]; node[mp(x[i], y[i])] = 1; } for (int i = 1; i <= n; ++ i) { for (int j = i + 1; j <= n; ++ j) { for (int k = 1; k <= n; ++ k) { if (i == k || j == k) continue; if (distance2(x[i], y[i], x[k], y[k]) == distance2(x[j], y[j], x[k], y[k])) { LL cp = crossProduct(x[j] - x[i], y[j] - y[i], x[k] - x[i], y[k] - y[i]); if (cp == 0) continue; ++ roof[i][j][cp > 0]; } } // std::cout << i << " " << j << " " << roof[i][j][0] << "-" << roof[i][j][1] << "\n"; } } LL ans = 0; for (int i = 1; i <= n; ++ i) { for (int j = i + 1; j <= n; ++ j) { for (int k = 1; k <= n; ++ k) { if (i == k || j == k) continue; if (dotProduct(x[j] - x[i], y[j] - y[i], x[k] - x[i], y[k] - y[i]) != 0) continue; LL xl = x[k] + x[j] - x[i]; LL yl = y[k] + y[j] - y[i]; if (!node.count(mp(xl, yl))) continue; LL cp = crossProduct(x[j] - x[i], y[j] - y[i], x[k] - x[i], y[k] - y[i]); if (cp == 0) continue; ans += roof[i][j][(cp > 0) ^ 1]; } } } std::cout << ans << "\n"; return 0; }
F
数论,搜索
实际大力题呃呃,妈的怎么这么多大力题
题目要求实际即下式:
即仅需保证:
- 中仅有 中的质因数。
- 中各质因数的次数不大于 中对应质因数的次数。
因为 的质因数至多只有不到 100 个,且有 ,由数学直觉可知实际上答案的数量级并不会很大,又发现给了 5s 的时限,于是考虑直接 DFS 大力枚举因数即可。可以保证一定仅会搜到有贡献的答案则复杂度并不会很高。
注意搜因数过程中可能爆 LL。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long #define i128 __int128 const int kN = 1e6; //============================================================= LL p, x, k, ans, prisz; std::vector<LL> pri; std::map<LL, int> cnt; //============================================================= void init() { LL temp = p; for (LL i = 2; i * i <= temp; ++ i) { if (temp % i != 0) continue; if (!cnt.count(i)) pri.push_back(i), cnt[i] = 0; while (temp % i == 0) temp /= i, cnt[i] = cnt[i] + 1; } if (temp != 1) { if (!cnt.count(temp)) pri.push_back(temp), cnt[temp] = 0; cnt[temp] = cnt[temp] + 1; } temp = k; for (LL i = 2; i * i <= temp; ++ i) { if (temp % i != 0) continue; if (!cnt.count(i)) pri.push_back(i); cnt[i] = kN; while (temp % i == 0) temp /= i; } if (temp != 1) { if (!cnt.count(temp)) pri.push_back(temp); cnt[temp] = kN; } } void dfs(int now_, i128 prod_) { if (prod_ > x) return ; if (now_ >= prisz) { ++ ans; // std::cout << prod_ << "\n"; return ; } for (int i = 0, sz = cnt[pri[now_]]; i <= sz && prod_ <= x; ++ i) { dfs(now_ + 1, prod_); prod_ *= pri[now_]; } } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); std::cin >> p >> x >> k; init(); prisz = pri.size(); // for (auto [a, b]: cnt) std::cout << a << " " << b << "\n"; dfs(0, 1); std::cout << ans << "\n"; return 0; }
L
计数,栈,括号序列。
发现对于一个括号嵌套的形式,一定是从内到外操作的。则实际上规定了操作的一个拓扑序,操作间构成了一个有根树的结构,不同子树间的操作是独立的,它们在操作序列中的顺序任意;有祖先关系的节点间必须保证操作顺序。
于是考虑按照括号的包含关系建树。因为加括号是每次往括号序列的末端加的,考虑倒序枚举括号序列进行操作序列的构造,并考虑枚举到的括号可以被插入到操作序列的什么位置,即按照树的先序遍历的倒序进行构造。对于倒序枚举到的某个节点 :
- 若为叶节点,则仅能插入到操作序列的开头,对答案贡献为 1;
- 若不为叶节点,则可插入操作序列的任意位置,对答案贡献为 。
贡献的乘积即为答案。
当然也可以直接考虑倒序枚举括号序列,根据组合意义进行推导,可以得到和上面的式子本质一样的做法。赛时 dztlb 大神就是这样直接倒着做就切了太牛逼了。
Code by dztlb:
#include<bits/stdc++.h> using namespace std; #define int long long const int lim=1e18; const int N=1e6+5; const int mod=998244353; inline int read(){ int x=0,f=1; char s; while((s=getchar())<'0'||s>'9') if(s=='-') f=-1; while(s>='0'&&s<='9') x=(x*10)+(s^'0'),s=getchar(); return x*f; } int n; char s[N]; int st[N],top; int a[N],tot; int id[N]; signed main(){ scanf("%s",s+1); n=strlen(s+1); top=1; st[1]=1; int cnt=0; for(int i=2;i<=n;++i){ if(s[i]==')'){ if(st[top]==i-1){ ++cnt; id[i]=cnt; --top; }else{ id[i]=id[i-1]; a[++tot]=id[i]-1; --top; } }else{ st[++top]=i; } } int ans=1; cnt--; for(int i=tot;i>=1;--i){ ans=ans*(cnt-a[i]+1+tot-i)%mod; } cout<<ans; return 0; } /* ((()())()())(()) (())() (())()()()()()()()()()()() */
K
贪心,构造
妈的这题怎么是 CCPC2024 网赛热身赛的原啊,妈的怎么能在一场比赛的赛时补另一场比赛的题啊呃呃,网赛前 4 天 vp 了这场然而没补这题输输输,然而热身赛把这题场切了赢赢赢
考虑按照 的权值递减对区间进行分层,并按照这个顺序进行构造,发现对于每一层的所有区间,一定被上一层的某个区间包含,于是仅需记录上一层的构造即可,且发现为了尽可能被上一层的区间包含,构造的区间的右端点应尽可能大。
然后考虑对于每一层按照 降序排序。由上述结论,对于第 0 层,为了保证每一层之间没有包含关系,一种最优的构造方案是:
对于第 层的左端点为 的区间,发现包含该区间的最优区间,一定是左端点小于 的、左端点最大的上一层的区间,即可据此贪心地确定该区间的右端点的取值。若在此过程中无法构造说明无解。
发现按照上述构造方法,每一层的区间在左端点递增的同时,右端点也一定是递增的,则由单调性,确定上一层包含当前区间的区间可以通过双指针简单维护。
为了保证区间不重合与合法需要讨论一些 的细节,详见代码。
总时间复杂度 级别,瓶颈在于排序。
热身赛赛时代码 by wenqizhi:
#include<bits/stdc++.h> using namespace std; #define ll long long int read() { int x = 0; bool f = false; char c = getchar(); while(c < '0' || c > '9') f |= (c == '-'), c = getchar(); while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar(); return f ? -x : x; } const int N = 1e5 + 5; int n; struct node { int l, r, b, id; node(){ l = r = b = id = 0; } }q[N]; bool cmp1(const node &a, const node &b) { if(a.b == b.b ) return a.l > b.l ; return a.b < b.b ; } bool cmp2(const node &a, const node &b) { return a.id < b.id ; } int l[N], r[N]; void solve() { n = read(); for(int i = 1; i <= n; ++i) { q[i].l = read(), q[i].b = read(); q[i].id = i; } sort(q + 1, q + n + 1, cmp1); int mx = 0; for(int i = 1; i <= n; ++i) { if(!l[q[i].b ]) l[q[i].b ] = i; r[q[i].b ] = i; mx = max(mx, q[i].b ); if(i > 1 && q[i].b == q[i - 1].b && q[i].l == q[i - 1].l ) { printf("-1\n"); return ; } } for(int i = 0; i < mx; ++i) { if(r[i] == 0) { printf("-1\n"); return ; } } q[1].r = 1e6; for(int i = 2; i <= r[0]; ++i) { q[i].r = q[i - 1].r - 1; if(q[i].l > q[i].r ) { printf("-1\n"); return ; } } for(int j = 1; j <= mx; ++j) { int pos = l[j - 1], last = 1e6 + 1; for(int i = l[j]; i <= r[j]; ++i) { while(pos <= r[j - 1] && q[pos].l > q[i].l ) ++pos; if(pos == l[j]) { printf("-1\n"); return ; } if(q[i].l == q[pos].l ) q[i].r = min(last - 1, q[pos].r - 1); else q[i].r = min(last - 1, q[pos].r ); last = q[i].r ; if(q[i].l > q[i].r ){ printf("-1\n"); return ; } } } sort(q + 1, q + n + 1, cmp2); for(int i = 1; i <= n; ++i) printf("%d\n", q[i].r ); } int main() { int T = 1; while(T--) solve(); return 0; }
I
计数,DP。
妈的 dztlb 大神最后爆切,可惜吃了 9 发呃呃唐。
显然考虑计数 DP。设当前填到了前缀 且令其合法的方案数为 。由性质可保证所有前缀的最后 个数一定构成大小为 的排列,于是考虑枚举填到 时最后一步添加了多少数 使得 也为排列。初始化 。
由于排列是不重复的,并不需要考虑之前的数的具体种类,仅需考虑其个数即可。于是记 表示在一个大小为 的排列后面添加 个数使得下列两条件成立的方案数:
- 新数列区间 构成大小为 的排列;
- 对于任意 , 不是排列,即保证添加 个数后最后一个长度为 的区间一定是好的,从而保证上述对 的定义的性质。
则有:
然后考虑如何预处理 ,显然此时添加 个数的权值是唯一的,于是考虑将添加的数看做 ,仅需考虑满足上述定义即可。发现上述限制等价于对于添加的 个数中构成的排列 ,任意小于 的前缀 不能是一个 的排列。于是考虑容斥,枚举每一个不合法排列最后一个违反限制的前缀。显然第一个不合法前缀为 时,方案数即为子问题 ,再乘上后面填数的方案数。则显然有:
于是做完了,式子很简洁。总时间复杂度 。
虽然场上写了快速取模但实际上常数并不大并无必要,不写也能跑过去。
Code by dztlb:
#include<bits/stdc++.h> using namespace std; #define int long long const int lim=1e18; const int N=1e6+5; const int mod=998244353; inline int read(){ int x=0,f=1; char s; while((s=getchar())<'0'||s>'9') if(s=='-') f=-1; while(s>='0'&&s<='9') x=(x*10)+(s^'0'),s=getchar(); return x*f; } int n,k; int a[N],fac[N]; typedef __int128_t i128; i128 _base=1; inline int mol(int x){return x-mod*(_base*x>>64);} int pre[N],onl[N]; signed main(){ _base=(_base<<64)/mod; cin>>n>>k; fac[0]=0;fac[1]=1; pre[1]=1; for(int i=2;i<=n;++i){ fac[i]=fac[i-1]*i%mod; pre[i]=mol(pre[i-1]+fac[i]); } if(n<k){ puts("0"); return 0; } if(n==k){ cout << fac[n] << endl; return 0; } a[k]=1; onl[1]=1; for(int i=2;i<=k;++i){ onl[i]=fac[i]; for(int j=1;j<i;++j){ onl[i]=mol(onl[i]-mol(onl[j]*fac[i-j])); } } a[k]=fac[k]; for(int x=k+1;x<=n;++x){ int tmp=0; for(int i=1;i<=k;++i){ if(x-i<k) break; a[x] += mol(a[x-i]*onl[i]), a[x] = mol(a[x]); } } cout<<a[n]<<endl; return 0; }
H
二分答案,图论
场上一直跟榜没开这题呃呃,我们是擅长这种经典题的没开到亏亏亏
为了方便讨论钦定 1 为根固定树的形态,考虑二分答案 ,枚举 条给定的路径 并检查:
- 若路径长度 ,则根可任意取。
- 否则汇合点一定在路径 上,考虑路径端点 到 的长度 :
- 若 ,则合法的汇合点一定在 到其 级祖先这条路径上,则 的 级祖先的子树内所有点都是合法的;
- 若 ,则合法的汇合点一定在 到 的 级祖先这条路径上,则除了 的 级祖先的子树内所有点都是合法的。
- 另一端点 同理。
若上述所有合法点集有交集,说明有解。发现上述过程中合法的点转化到 dfs 序上至多只有 2 段连续的区间,于是直接差分实现即可。
需要求 和节点的 级祖先,使用倍增实现,总时间复杂度 级别。
妈的又没特判 0 挂了一发太唐乐。
// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 2e5 + 10; //============================================================= int n, m, x[kN], y[kN], lca[kN], dis[kN]; int edgenum, head[kN], v[kN << 1], ne[kN << 1]; int dfnnum, fa[kN][21], dep[kN], dfn[kN], L[kN], R[kN]; int d[kN]; //============================================================= void Add(int u_, int v_) { v[++ edgenum] = v_; ne[edgenum] = head[u_]; head[u_] = edgenum; } void Dfs(int u_, int fa_) { L[u_] = dfn[u_] = ++ dfnnum; dep[u_] = dep[fa_] + 1; fa[u_][0] = fa_; for (int i = 1; i <= 18; ++ i) { fa[u_][i] = fa[fa[u_][i - 1]][i - 1]; } for (int i = head[u_]; i; i = ne[i]) { int v_ = v[i]; if (v_ == fa_) continue; Dfs(v_, u_); } R[u_] = dfnnum; } int Lca(int u_, int v_) { if (dep[u_] < dep[v_]) std::swap(u_, v_); for (int i = 18; i >= 0; -- i) { if (dep[fa[u_][i]] >= dep[v_]) { u_ = fa[u_][i]; } } if (u_ == v_) return u_; for (int i = 18; i >= 0; -- i) { if (fa[u_][i] != fa[v_][i]) { u_ = fa[u_][i]; v_ = fa[v_][i]; } } return fa[u_][0]; } int get(int u_, int k_) { for (int i = 18; i >= 0; -- i) { if (k_ >= (1 << i)) { u_ = fa[u_][i]; k_ -= (1 << i); } } return (k_ == 0) * u_; } int Dis(int u_, int v_) { return dep[u_] + dep[v_] - 2 * dep[Lca(u_, v_)]; } bool check(int mid_) { for (int i = 0; i <= n; ++ i) d[i] = 0; for (int i = 1; i <= m; ++ i) { if (dis[i] <= mid_) { d[0] += 2; continue; } if (dep[x[i]] - dep[lca[i]] > mid_) { int p = get(x[i], mid_); ++ d[L[p]], -- d[R[p] + 1]; } else { int p = get(y[i], dis[i] - mid_ - 1); if (!p) return false; ++ d[0], -- d[L[p]], ++ d[R[p] + 1]; } if (dep[y[i]] - dep[lca[i]] > mid_) { int p = get(y[i], mid_); ++ d[L[p]], -- d[R[p] + 1]; } else { int p = get(x[i], dis[i] - mid_ - 1); if (!p) return false; ++ d[0], -- d[L[p]], ++ d[R[p] + 1]; } } for (int i = 1; i <= n; ++ i) d[i] += d[i - 1]; for (int i = 0; i <= n; ++ i) if (d[i] == 2 * m) return true; return false; } //============================================================= int main() { // freopen("1.txt", "r", stdin); std::ios::sync_with_stdio(0), std::cin.tie(0); std::cin >> n >> m; for (int i = 1; i < n; ++ i) { int u_, v_; std::cin >> u_ >> v_; Add(u_, v_), Add(v_, u_); } Dfs(1, 0); for (int i = 1; i <= m; ++ i) { std::cin >> x[i] >> y[i]; lca[i] = Lca(x[i], y[i]); dis[i] = Dis(x[i], y[i]); } int ans = n; for (int l = 0, r = n; l <= r; ) { int mid = (l + r) >> 1; if (check(mid)) { ans = mid; r = mid - 1; } else { l = mid + 1; } } std::cout << ans << "\n"; return 0; }
写在最后
学到了什么:
- H:对点集的标记转化到 dfs 序上进行。
- E、H:考虑答案能否取到边界情况!0!0!0!
我是???
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!