ICPC2026 深圳邀请赛题解 AEDFGHIL
The 2026 ICPC China Shenzhen Invitational Contest
ABCDEFGHIJKLM
A. Greetings from Prof. Chen
给一个数字 \(n\),构造一个只由小写英文字母构成的字符串,使其字母 \(\text{ASCII}\) 码的和是 \(n\)。\(1\leq n\leq 10^7\)。或判断不可能。
直接定义 \(dp_i=c\) 表示和为 \(i\) 的字符串可由和为 \(i-c\) 的字符串通过在末尾添加一个字符 \(c\) 表示。由于 \(i\) 很大的时候基本都可以表示,于是不会超时。
#include <bits/stdc++.h>
const int N = 1e7 + 5;
int pre[N];
void init() {
pre[0] = -2;
for (int i = 1; i <= 96; i ++)
pre[i] = -1;
for (int i = 97; i <= 10000000; i ++) {
pre[i] = -1;
for (int j = 97; j <= 122; j ++) {
if (pre[i - j] != -1) {
pre[i] = i - j;
break;
}
}
}
}
int n;
void work() {
scanf("%d", &n);
if (pre[n] == -1) {
puts("No");
return;
}
puts("Yes");
std::string s = "";
while (pre[n] != -2) {
s = char(n - pre[n]) + s;
n = pre[n];
}
std::cout << s << "\n";
}
int main() {
init();
int T;
scanf("%d", &T);
while (T --)
work();
return 0;
}
L. Critical Strike
商店有 \(n\) 件装备,第 \(i\) 件以 \(\frac{p_i}{100}\) 概率触发暴击(倍率 \(v_i\)),花费 \(w_i\),限购一次。同时触发时取最高倍率,无触发则倍率为 \(0\)。最多花 \(m\) 金币,求最大期望倍率。
\(p_i,v_i,w_i\)(\(p_i\in[1,100],\,v_i\ge1,\,w_i\ge1\))。 \(\sum n,\sum m\le 3\times10^3\)。
按 \(v_i\) 排序,则如果后面的装备被触发,则可以无需考虑前面的倍率。
注意一个装备只能购买一次,于是定义 \(dp_{i,j}\) 为花费 \(i\) 枚金币,取到第 \(j\) 件装备时的最大期望。具体实现见代码。
int n,m;
struct node{
double p,v;int w;
}a[N];
bool cmp(node x,node y){
return x.v<y.v;
}
double dp[N][N];
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&m);
for(int i=0;i<=m;++i){
for(int j=0;j<=n;++j)
dp[i][j]=0;
}
for(int i=1;i<=n;++i){
scanf("%lf%lf%d",&a[i].p,&a[i].v,&a[i].w);
a[i].p=a[i].p*0.01;
}
sort(a+1,a+1+n,cmp);
for(int i=1;i<=m;++i){
for(int j=1;j<=n;++j){
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(a[j].w>i)continue;
dp[i][j]=max(dp[i][j],(1-a[j].p)*dp[i-a[j].w][j-1]+a[j].p*a[j].v);
}
}
printf("%.10lf\n",dp[m][n]);
}
return 0;
}
I. Calendar Cubes
给定两位数 \(x\)(\(01\le x\le 99\))。每个骰子 \(6\) 面,每面可填数字 \(0\)–\(8\)(\(6\) 可当作 \(9\))。两个骰子可拼出 \(01\)–\(99\) 的两位数(一个作十位,一个作个位)。定义 \(\text{MEX}\) 为最小的无法拼出的 正 两位数。要求构造两个骰子使得 \(\text{MEX}=x\)。若能,输出
Yes及 \(12\) 个数字(前 \(6\) 个为第一个骰子,后 \(6\) 个为第二个);否则输出No。
枚举两个骰子每个骰子可以表示哪些数(每个骰子至多 \(2^9\) 种状态,表示哪些数出现过),这样总共就只有 \(2^{18}\) 种状态,然后判断一下该状态的 \(\text{MEX}\),汇总一下即可。
#include <bits/stdc++.h>
const int S = 512 + 5;
const int N = 100 + 5;
int mex[S];
bool ok[N];
bool sb[S];
std::vector<int> ans[N];
bool u[N];
void init() {
for (int i = 1; i < (1 << 9); i ++) {
int c = 0;
mex[i] = 9;
for (int j = 0; j <= 8; j ++) {
if ((i >> j) & 1) {
c ++;
} else {
mex[i] = std::min(mex[i], j);
}
}
if (c > 6) {
sb[i] = 1;
continue;
}
}
for (int x = 1; x < (1 << 9); x ++) if (!sb[x]) {
for (int y = 1; y < (1 << 9); y ++) if (!sb[y]) {
for (int j = 0; j <= 99; j ++) u[j] = 0;
std::vector<int> s1, s2;
for (int k = 0; k <= 8; k ++) if ((x >> k) & 1) {
s1.push_back(k);
if (k == 6)
s1.push_back(9);
}
for (int k = 0; k <= 8; k ++) if ((y >> k) & 1) {
s2.push_back(k);
if (k == 6)
s2.push_back(9);
}
for (auto n1 : s1)
for (auto n2 : s2)
u[n1 * 10 + n2] = u[n2 * 10 + n1] = 1;
int mx = 0;
while (u[mx])
mx ++;
if (ok[mx])
continue;
ok[mx] = 1;
for (int k = 0; k <= 8; k ++) if ((x >> k) & 1)
ans[mx].push_back(k);
while (ans[mx].size() < 6)
ans[mx].push_back(ans[mx].back());
for (int k = 0; k <= 8; k ++) if ((y >> k) & 1)
ans[mx].push_back(k);
while (ans[mx].size() < 12)
ans[mx].push_back(ans[mx].back());
}
}
}
int main() {
init();
int T; scanf("%d", &T);
while (T --) {
int n;
scanf("%d", &n);
if (!ok[n]) {
puts("No");
continue;
}
puts("Yes");
for (auto x : ans[n])
printf("%d ", x);
puts("");
}
return 0;
}
G. Snake
交互题。给一个 \(n\times m\) 的网格,请你扮演一只贪吃蛇吃苹果。每次会有一个苹果出现在屏幕中蛇没覆盖的位置,然后你输出一个长度不大于 \(n\) 的
LRUD操作序列去吃到那个苹果。蛇初始长度为 \(1\),位置给出,一共吃 \(nm-1\) 个苹果。
玩过贪吃蛇的应该直觉上就能理解这个题就是找一个可以覆盖所有位置的循环。行列数之一为偶数的时候比较好做,存在这样一个循环。如图所示。

当 \(n\) 为奇数的时候,可以找到这样一个循环,只遗漏了 \((2,2)\) 一个点。

但是当我们移动到 \((1,2)\) 时,可以选择向下移动到 \((2,2)\) 再移动到 \((2,1)\) 回到循环,可以发现不会撞到。
然后就是实现问题了。给一份丑陋的代码。
const int dx[4]={1,0,-1,0};
const int dy[4]={0,1,0,-1};
const char opt[4]={'D','R','U','L'};
bool flag=1;
int n,m;
ttfa lis[N],head;int tot;
char val[N];
char step(ttfa x,ttfa y){
for(int i=0;i<4;++i)
if(x[0]+dx[i]==y[0]&&x[1]+dy[i]==y[1])
return opt[i];
return 'W';
}
void evenlis(){
for(int i=1;i<=n;++i)lis[++tot]={i,1};
for(int i=n;i>=1;--i){
if(i&1){
for(int j=m;j>=2;--j)lis[++tot]={i,j};
}else{
for(int j=2;j<=m;++j)lis[++tot]={i,j};
}
}
}
int query(ttfa x){
for(int i=1;i<=tot;++i){
if(lis[i]==x)
return i;
}
return 0;
}
char s[N];int top;
void evensolve(){
for(int i=1;i<=tot;++i){
int j=i%tot+1;
val[i]=step(lis[i],lis[j]);
}
for(int t=1;t<=n*m-1;++t){
ttfa pos;scanf("%d%d",&pos[0],&pos[1]);
int now=query(head),loc=query(pos);top=0;
while(now!=loc){
s[++top]=val[now];
now=now%tot+1;
}
head=pos;
for(int i=1;i<=top;++i)
printf("%c",s[i]);
printf("\n");
fflush(stdout);
}
}
void oddsolve(){
for(int i=1;i<=n;++i)lis[++tot]={i,1};
for(int i=n;i>=3;--i){
if(i&1){
for(int j=2;j<=m;++j)lis[++tot]={i,j};
}else{
for(int j=m;j>=2;--j)lis[++tot]={i,j};
}
}
for(int j=m;j>=3;--j){
if(j&1){
lis[++tot]={2,j},lis[++tot]={1,j};
}else{
lis[++tot]={1,j},lis[++tot]={2,j};
}
}lis[++tot]={1,2};
for(int i=1;i<=tot;++i){
int j=i%tot+1;
val[i]=step(lis[i],lis[j]);
}
for(int t=1;t<=n*m-1;++t){
ttfa pos;scanf("%d%d",&pos[0],&pos[1]);
int now=query(head),loc=query(pos);top=0;
if(now==0){s[++top]='L';now=2;}
if(loc!=0){
while(now!=loc){
s[++top]=val[now];
now=now%tot+1;
}
}else{
for(int i=now;i<tot;++i){
s[++top]=val[i];
}
s[++top]='D';
}
head=pos;
for(int i=1;i<=top;++i)
printf("%c",s[i]);
printf("\n");
fflush(stdout);
}
}
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%d%d%d%d",&n,&m,&head[0],&head[1]);
tot=0;
if(n%2==0){
evenlis();
}else if(m%2==0){
swap(n,m);
evenlis();
swap(n,m);
for(int i=1;i<=tot;++i){
swap(lis[i][0],lis[i][1]);
}
}
if(tot){
evensolve();
}else{
oddsolve();
}
}
return 0;
}
D. City Management
\(n\) 天,每天事件:
W(来一个工人,之后可重新分配所有工人)或F x(建一个容量 \(x\) 的工厂,只能从空闲工人中选一人入职)。每厂最多一人,有工人则获利 \(x\),否则 \(0\)。每天事件后统计一下利润。求最大总利润。\(n\leq 2\times 10^5\)。
这题的核心问题就是对于连续的一段工厂,在这个过程中不能重新分配工人。
对于连续一段工厂的倒数第 \(i\) 个工厂,如果他被选中有工人,则会产生 \(i\times x_i\) 的贡献;对于这一段连续工厂之前的工厂,则会产生 \(len\times x_i\),其中 \(len\) 为这段工厂的长度。于是对于当前段连续之前的工厂,使用权值线段树维护是否存在;对于当前连续段的工厂,按 \(i\times x_i\) 从大到小排序后进行选择,选择的过程中也要把权值线段树中的工厂纳入考虑,直到剩余的工人不够。
const int MAXN = 2e5+5;
const int mod = 998244353;
int n, N;
struct Query {
int op, x;
} Q[MAXN];
vector <int> val;
vector <ll> vec;
map <int,int> mp;
struct node {
ll cnt, sum;
} t[MAXN<<2];
void pushUp(int o) {
t[o].cnt = t[o<<1].cnt + t[o<<1|1].cnt;
t[o].sum = t[o<<1].sum + t[o<<1|1].sum;
}
void build(int o, int l, int r) {
if(l == r) {
t[o].cnt = t[o].sum = 0;
return;
}
int mid = (l+r)>>1;
build(o<<1, l, mid);
build(o<<1|1, mid+1, r);
pushUp(o);
}
void update(int o, int l, int r, int pos, int val) {
if(l == r) {
++ t[o].cnt;
t[o].sum += val;
return;
}
int mid = (l+r)>>1;
if(pos <= mid) update(o<<1, l, mid, pos, val);
else update(o<<1|1, mid+1, r, pos, val);
pushUp(o);
}
int queryCnt(int o, int l, int r, int pos) {
if(l == r) return t[o].cnt;
int mid = (l+r)>>1;
if(pos > mid) return queryCnt(o<<1|1, mid+1, r, pos);
return t[o<<1|1].cnt + queryCnt(o<<1, l, mid, pos);
}
ll query(int o, int l, int r, int tot) {
if(l == r) {
return min(tot+0ll, t[o].cnt)*val[l-1];
}
int mid = (l+r)>>1;
if(tot > t[o<<1|1].cnt) return t[o<<1|1].sum + query(o<<1, l, mid, tot-t[o<<1|1].cnt);
return query(o<<1|1, mid+1, r, tot);
}
void noSananoLife() {
n = read(9); char opt[10];
val.clear();
mp.clear();
for(int i=1; i<=n; ++i) {
scanf("%s", opt);
Q[i].op = (opt[0]=='F'? 1: 0);
if(Q[i].op)
Q[i].x=read(9), val.push_back(Q[i].x);
}
sort(val.begin(), val.end());
val.erase(unique(val.begin(), val.end()), val.end());
for(int i=0, siz=val.size(); i<siz; ++i)
mp[val[i]] = i+1;
int lasW = 0, allW = 0;
if(val.empty()) return puts("0"), void();
build(1, 1, N=val.size());
ll ans = 0;
for(int i=1; i<=n; ++i) {
while(i<=n && Q[i].op) ++ i;
vec.clear();
for(int j=lasW+1; j<i; ++j) {
vec.push_back(1ll*Q[j].x*(i-j));
}
sort(vec.begin(), vec.end());
int pre = 0; ll preSum = 0;
for(int j=int(vec.size())-1; j>=0; --j) {
ll lim = (ll)ceil((double)vec[j]/(i-lasW));
int pos = lower_bound(val.begin(), val.end(), lim) - val.begin();
int cnt = 0;
if(pos != val.size()) {
cnt = queryCnt(1, 1, N, pos+1);
}
if(cnt+pre+1 > allW) break;
++ pre;
preSum += vec[j];
}
ans += preSum+query(1, 1, N, allW-pre)*(i-lasW);
for(int j=lasW+1; j<i; ++j) {
update(1, 1, N, mp[Q[j].x], Q[j].x);
}
lasW = i; ++ allW;
}
if(Q[n].op == 0) {
ans += query(1, 1, N, allW);
}
print(ans, '\n');
}
int main() {
for(int T=read(9); T; --T)
noSananoLife();
return 0;
}
E. Card Checking
三人斗地主:A、B 为农民(任一清空则农民胜),C 为地主(C 清空则地主胜)。牌点 \(1\) 到 \(n\),只能出单张或对子(对子不可拆)。手牌公开,回合制,A 先出,后续按 A→B→C 顺序。只能出相同类型且更大点数,或过。一轮结束后最后出牌的人开始下一轮。已知 C 只剩一张点数为 \(p_c\) 的牌,A、B 的手牌由长度 \(n\) 的字符串 \(s_a,s_b\) 给出(字符 0/1/2 表示该点数的张数)。问农民能否必胜。
首先先特判 B 可以获胜的情况,由于 B 打完牌后即是 C 接牌,于是 B 如果有大于两张小于 \(p_c\) 的单牌,则他必不能走完。如果现在小于 \(p_c\) 的单牌数量小于等于 \(1\),只要 A 能让 B 变成先手,B 就能一直打牌(A 不接),最后把那张小于 \(p_c\) 的单排打出去获胜。于是 B 可胜利的条件为:
- 小于 \(p_c\) 的单牌数量小于等于 \(1\);同时
-
- 存在一个对子大于 A 手中的一个对子
- 存在一张单牌大于 A 手中的某张单牌
- 这张单牌大于等于 \(p_c\);或者
- 没有对子,只有一张牌,而这张牌就是那张小于 \(p_c\) 的牌。
接下来考虑 A 怎么胜利。我们的目的就是尽量多打出去比 \(p_c\) 小的牌,且让 A 占据主动权;最后通过连续让 A 打对子和大于等于 \(p_c\) 的牌,最后打一张小于 \(p_c\) 的牌获胜。
可以发现,让 A,B 两人连续打牌是不会比 \(A\to B\to A'\) 这样打牌更优的,因为这样所消耗的 B 牌更少,走掉的 A 牌更多。于是我们先统计这种情况的总数,需要满足 \(A<p_c\leq B<A'\)。在这个过程中,我们希望 \(B\) 尽可能小,以有更多的 \(A'\) 可以接上。
接下来可能 \(A'\) 打光了只能打 \(A\to B\) 这样的组合,但是每次进行过后变成了 \(B\) 打牌,\(A\) 失去了主动权,这时可以利用对子创造主动权。于是可以打出的 \(A\) 的牌数就是 \(A\to B\) 组合数和对子组合数。
具体实现见代码。
#include <bits/stdc++.h>
const int N = 3e5 + 5;
int n;
char a[N], b[N];
int pc;
bool u[N];
void work() {
scanf("%d", &n);
scanf("%s", a + 1);
scanf("%s", b + 1);
scanf("%d", &pc);
{
int c = 0;
for (int i = 1; i < pc; i ++) if (b[i] == '1') // B < pc
c ++;
if (c <= 1) {
int ma1 = n + 1, ma2 = n + 1; // min
int mb1 = 0, mb2 = 0; // max
for (int i = n; i >= 1; i --) {
if (a[i] == '1')
ma1 = i;
else if (a[i] == '2')
ma2 = i;
}
for (int i = 1; i <= n; i ++) {
if (b[i] == '1')
mb1 = i;
else if (b[i] == '2')
mb2 = i;
}
if (ma2 < mb2 or (ma1 < mb1 and mb1 >= pc) or (ma1 < mb1 and !mb2 and mb1 < pc)) {
puts("Yes");
return;
}
}
}
int c1 = 0; // A count(< pc)
for (int i = 1; i < pc; i ++) if (a[i] == '1')
c1 ++;
{
for (int i = 1; i <= n; i ++)
u[i] = 0;
int c2 = 0;
int i = 0, j = pc; // i -> (j -> i) pair
while (i <= n and j <= n) {
while (j <= n and b[j] != '1')
j ++;
i = std::max(i+1, j + 1);
while (i <= n and a[i] != '1')
i ++;
if (i <= n) {
u[j] = 1;
c2 ++;
}
j ++;
}
int c3 = 0; // 2 j -> i pair
i = 0, j = 1;
while (i <= n and j <= n) {
while (j <= n and b[j] != '2')
j ++;
i = std::max(i+1,j + 1);
while (i <= n and a[i] != '2')
i ++;
if (i <= n)
c3 ++;
j ++;
}
int c4 = 0; // i -> j pair
for (int i = pc; i <= n; i ++) if (b[i] == '1' and !u[i])
c4 ++;
if (c2 + std::min(c3, c4) >= c1 - 1) {
puts("Yes");
} else {
puts("No");
}
}
}
int main() {
int T;
scanf("%d", &T);
while (T --)
work();
return 0;
}
vp 时队友双指针写错了,看一年没看出来,太痛了
H. Telepathy
通信题。Christina 选大小 \(k<\frac{n}{2}\) 的子集 \(S\subseteq\{1,\dots,n\}\)。Alice 添加 \(x\notin S\) 得 \(T=S\cup\{x\}\)。Bob 看到 \(T\) 需猜 \(x\)。两阶段运行:第一次(Alice)输入
Alice、\(T\)、\(n,k\) 和 \(S\),输出 \(T\)。第二次(Bob)输入Bob、\(T\)、\(n,k\) 和 \(T\)(顺序可能不同),输出 \(x\)。\(n\) 总和 \(\le 3\times10^6\)。
神仙题,根本想不出来,以下内容直接抄的官方题解:
将 \(1\) 到 \(n\) 排成序列,在集合中的元素视为左括号,不在集合中的视为右括号,从左到右贪心做括号匹配。
对原始集合 \(S\),有 \(k\) 个左括号和 \((n-k)\) 个右括号。因为 \(k < \frac{n}{2}\),所有左括号都能被匹配,剩下 \((n-2k)\) 个未匹配的右括号。
First Run 的策略:选择最后一个未匹配的右括号 \(x\) 加入集合。复杂度 \(O(n)\)。
Second Run 的策略:对收到的 \(S'\) 做同样的括号匹配,输出栈底。复杂度 \(O(n)\)。
代码
#include <bits/stdc++.h>
int n, k;
int main() {
std::string op;
std::cin >> op;
if (op == "Alice") {
int T;
scanf("%d", &T);
while (T --) {
scanf("%d%d", &n, &k);
std::string t = "";
for (int i = 0; i < n; i ++)
t += ')';
std::set<int> set;
for (int i = 1; i <= k; i ++) {
int x;
scanf("%d", &x);
set.insert(x);
}
for (auto x : set)
t[x - 1] = '(';
int cnt = 0, max = 0;
for (int i = 0; i < n; i ++) {
if (t[i] == '(')
cnt ++;
else {
if (cnt)
cnt --;
else
max = i + 1;
}
}
set.insert(max);
for (auto x : set)
printf("%d ", x);
puts("");
}
} else {
int T;
scanf("%d", &T);
while (T --) {
scanf("%d%d", &n, &k);
std::string t = "";
for (int i = 0; i < n; i ++)
t += ')';
std::set<int> set;
for (int i = 1; i <= k + 1; i ++) {
int x;
scanf("%d", &x);
set.insert(x);
}
for (auto x : set)
t[x - 1] = '(';
std::stack<int> stack;
for (int i = 0; i < n; i ++) {
if (t[i] == '(')
stack.push(i + 1);
else {
if (!stack.empty())
stack.pop();
}
}
while (stack.size() != 1)
stack.pop();
printf("%d\n", stack.top());
}
}
return 0;
}
F. Astra
树上有 \(n\) 个节点,初始只标记 \(s\)。两人轮流操作,每轮可标记 \(1\) 到 \(k\) 个新节点,按顺序每个新节点必须与上一标记节点相邻(即形成一条简单路径)。标记最后节点者胜。对每个 \(s\)(\(1\le s\le n\)),在双方最优下判断谁赢。
SG 函数模板题,可惜赛时没开到这个题(主要是看错题了qwq)。对于一颗子树,他的某个后继状态为,除去选取的简单路径后,得到若干个子子树,这种情况的状态即为这些子子树的 SG 函数值 Nim 和。然后这个子树的 SG 函数值就是所有后继状态(即所有长度 \(\leq k\) 简单路径的选择)SG 函数值的 \(\text{mex}\) 值。
但是这样做每次不同的根做一次的复杂度都是 \(O(n^2)\) 的,总复杂度 \(O(n^3)\),可以使用记忆化搜索,\(dp_{u,v}\) 记录 \(u\to v\) 时以 \(v\) 为根的子树的 SG 函数值。
主要难度在于实现,这里给出一种自认为比较好的写法。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int>ttfa;
const int N=2003;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n,k;
vector<int>tar[N];
int dp[N][N],ans[N];
int getmex(vector<int>bot){
for(int i=0;i<=n;++i)
if(!bot[i])return i;
return -1;
}
int dfsp(int f,int u){
if(dp[f][u]!=-1)return dp[f][u];
vector<int>bot(n+1,0);int sg=0;
function<void(int,int,int)> dfs=[&](int las,int v,int d){
int tmp=0;
for(auto w:tar[v]){
if(w==las)continue;
tmp^=dfsp(v,w);
}
sg^=tmp;
++bot[sg];
if(d>=k){
sg^=tmp;
return;
}
for(auto w:tar[v]){
if(w==las)continue;
sg^=dfsp(v,w);
dfs(v,w,d+1);
sg^=dfsp(v,w);
}
sg^=tmp;
};
dfs(f,u,1);
return dp[f][u]=getmex(bot);
}
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i){
tar[i].clear();ans[i]=0;
for(int j=1;j<=n;++j)
dp[i][j]=-1;
}
for(int i=1;i<n;++i){
int u,v;scanf("%d%d",&u,&v);
tar[u].push_back(v);
tar[v].push_back(u);
}
for(int x=1;x<=n;++x){
int sg=0;
for(auto v:tar[x]){
sg^=dfsp(x,v);
}
ans[x]=sg?1:0;
}
for(int i=1;i<=n;++i){
printf("%d",ans[i]);
}puts("");
}
return 0;
}
算是西安邀请赛 Day0 写的,不是很详尽,还有一些能补的题没写,以后再补吧。

浙公网安备 33010602011771号