The 2022 ICPC Asia Jinan Regional Contest
Preface
在 QOJ 上翻近几年的 Regional 时发现好像有场之前没 VP 过的漏网之鱼,遂 VP 之
这场感觉大家状态都挺在线的,会的题基本上都没怎么卡住,最后低罚时 8 个题,放在当年的现场可能就出线了,感觉很牛啊
最后会的 H 题也看出了所有转化,只可惜时间原因没写完
A. Tower
先不考虑移除 \(m\) 个的操作时怎么处理
根据经典 trick,先进行除以 \(2\) 操作再进行加减操作一定是不劣的
而只使用加减操作时,不难发现最少的操作步数就是把所有数变成序列的中位数
因此不难发现最后可能得到的数只有 \(O(n\log a_i)\) 种可能(即每个数一直除以 \(2\) 下取整得到的数)
而固定了最后得到的数后每个位置需要的操作次数很容易 \(O(\log a_i)\) 算出
最后我们再刨除掉值最大的 \(m\) 个位置即可
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=505,INF=1e18;
int t,n,m,a[N];
signed main()
{
for (scanf("%lld",&t);t;--t)
{
scanf("%lld%lld",&n,&m);
vector <int> vec;
for (RI i=1;i<=n;++i)
{
scanf("%lld",&a[i]);
int x=a[i];
do
{
vec.push_back(x);
x>>=1;
} while (x!=0);
}
sort(vec.begin(),vec.end());
vec.erase(unique(vec.begin(),vec.end()),vec.end());
int ans=INF;
for (auto tar:vec)
{
vector <int> cost;
auto solve=[&](int x,CI y)
{
int res=INF,c=0;
do
{
res=min(res,c+abs(x-y));
x>>=1; ++c;
} while (x>=y);
if (x>0) res=min(res,c+abs(x-y));
return res;
};
for (RI i=1;i<=n;++i)
cost.push_back(solve(a[i],tar));
sort(cost.begin(),cost.end());
int res=0;
for (RI i=0;i<n-m;++i) res+=cost[i];
ans=min(ans,res);
}
printf("%lld\n",ans);
}
return 0;
}
C. DFS Order 2
考虑 DP,首先不难算出每个子树的 DFS 序总数
令 \(f_{i,j}\) 表示 \(i\) 出现在 DFS 序第 \(j\) 个位置的方案数,不难发现一个点的位置只和根到它路径有关
更进一步地,我们考虑从 \(x\) 转移到其儿子 \(y\) 时,\(f_{x,i}\) 对 \(f_{y,j}\) 的影响
一个显然的问题是我们要把中间空余的 \(j-i-1\) 个位置用 \(x\) 除了 \(y\) 以外的子树填满
通过维护一个二维的背包,第一维是子树数量,第二维是子树点数和即可处理这部分的贡献
但每次暴力重新算一遍背包复杂度是 \(O(n^4)\) 的,徐神提出计数背包可以直接逆操作撤回一个元素,优化掉这部分后就变成 \(O(n^3)\) 的了
#include <bits/stdc++.h>
using llsi = long long signed int;
constexpr int $n = 505;
constexpr int mod = 998244353;
constexpr int ksm(int a, int b) {
int c = 1;
while(b) {
if(b & 1) c = llsi(c) * llsi(a) % mod;
a = llsi(a) * llsi(a) % mod;
b >>= 1;
}
return c;
}
struct mi {
int value;
inline mi(int value = 0): value(value) {}
inline operator int() { return value; }
inline friend mi operator +(mi a, mi b) {
int res = a.value + b.value;
if(res >= mod) res -= mod;
return mi{res};
}
inline friend mi operator -(mi a, mi b) {
int res = a.value - b.value;
if(res < 0) res += mod;
return mi{res};
}
inline friend mi operator *(mi a, mi b) {
return mi{int(llsi(a.value) * llsi(b.value) % mod)};
}
inline mi inv() {
return mi{ksm(value, mod - 2)};
}
};
int n;
std::vector<int> out[$n];
int siz[$n];
mi f[$n][$n];
mi fac[$n], facinv[$n];
void prep(int N = 500) {
fac[0] = mi{1};
for(int i = 1; i <= N; ++i) fac[i] = fac[i - 1] * mi{i};
facinv[N] = fac[N].inv();
for(int i = N; i >= 1; --i) facinv[i - 1] = facinv[i] * mi{i};
return ;
}
inline mi C(int a, int b) { return fac[a] * facinv[b] * facinv[a - b]; }
mi dfs1(int cur, int fa) {
int chcnt = out[cur].size() - (cur != 1);
mi res{1}; siz[cur] = 1;
for(auto ch: out[cur]) if(ch != fa)
res = res * dfs1(ch, cur),
siz[cur] += siz[ch];
return res * fac[chcnt];
}
static mi g[$n][$n];
void g_add(int val, int x, int y) {
for(int i = x - 1; i >= 0; --i) for(int j = y - val; j >= 0; --j)
g[i + 1][j + val] = g[i + 1][j + val] + g[i][j];
}
void g_del(int val, int x, int y) {
for(int i = 0; i <= x - 1; ++i) for(int j = 0; j <= y - val; ++j)
g[i + 1][j + val] = g[i + 1][j + val] - g[i][j];
}
void dfs2(int cur, int fa) {
int chcnt = out[cur].size() - (cur != 1);
memset(g, 0, sizeof(mi) * $n * (chcnt + 1));
g[0][0] = 1;
int chcnt2 = 0, sizcnt2 = 1;
for(auto ch: out[cur]) if(ch != fa)
g_add(siz[ch], chcnt2 += 1, sizcnt2 += siz[ch]);
for(auto ch: out[cur]) if(ch != fa) {
g_del(siz[ch], chcnt, siz[cur]);
for(int i = 1; i <= n - siz[cur] + 1; ++i) for(int j = i + 1; j <= n - siz[ch] + 1; ++j) {
for(int k = 0; k < chcnt; ++k) {
f[ch][j] = f[ch][j] + f[cur][i] *
(fac[k] * fac[chcnt - k - 1] * g[k][j - i - 1] * facinv[chcnt]);
}
}
g_add(siz[ch], chcnt, siz[cur]);
}
for(auto ch: out[cur]) if(ch != fa)
dfs2(ch, cur);
return ;
}
int main() {
std::ios::sync_with_stdio(false);
prep();
std::cin >> n;
for(int i = 1, u, v; i < n; ++i) {
std::cin >> u >> v;
out[u].emplace_back(v);
out[v].emplace_back(u);
}
f[1][1] = dfs1(1, 0);
dfs2(1, 0);
for(int i = 1; i <= n; ++i) for(int j = 1; j <= n; ++j)
std::cout << int(f[i][j]) << char(j == n ? 10 : 32);
return 0;
}
D. Frozen Scoreboard
大力模拟题,简单分析会发现一个 ? x y 的题如果通过的话贡献的罚时是一个连续区间 \([240+(y-x)\times 20,299+(y-1)\times 20]\)
因此大力子集枚举哪些 ? 的题是过了的,将它们可能的罚时区间合并后检验最终罚时是否包含在内即可,同时构造答案是 trivial 的
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=15;
int n,m,L[N],R[N];
int main()
{
for (scanf("%d%d",&n,&m);n;--n)
{
int final_num,final_pen;
scanf("%d%d",&final_num,&final_pen);
int cur_num=0,cur_pen=0;
static int state[N],tx[N],ty[N];
vector <int> unkn;
for (RI i=0;i<m;++i)
{
char opt[5];
scanf("%s",opt);
if (opt[0]=='+')
{
state[i]=1; int x,y;
scanf("%d/%d",&x,&y);
tx[i]=x; ty[i]=y;
++cur_num; cur_pen+=y+(x-1)*20;
} else if (opt[0]=='?')
{
state[i]=2; int x,y;
scanf("%d%d",&x,&y);
tx[i]=x; ty[i]=y;
unkn.push_back(i);
L[i]=240+(y-x)*20;
R[i]=299+(y-1)*20;
} else if (opt[0]=='-')
{
state[i]=3; int x;
scanf("%d",&x); tx[i]=x;
} else state[i]=4;
}
if (cur_num>final_num) { puts("No"); continue; }
int flag=0,unkn_num=(int)unkn.size();
for (RI mask=0;mask<(1<<unkn_num)&&!flag;++mask)
{
if (cur_num+__builtin_popcount(mask)!=final_num) continue;
int lb_pen=cur_pen,rb_pen=cur_pen;
for (RI i=0;i<unkn_num;++i)
if ((mask>>i)&1)
{
int id=unkn[i];
lb_pen+=L[id]; rb_pen+=R[id];
}
// printf("lb = %d, rb = %d\n",lb_pen,rb_pen);
if (lb_pen<=final_pen&&final_pen<=rb_pen)
{
flag=1;
int left=final_pen-lb_pen;
for (RI i=0;i<unkn_num;++i)
{
int id=unkn[i];
if ((mask>>i)&1)
{
state[id]=1;
int tmp=min(left,R[id]-L[id]);
left-=tmp;
int true_pen=L[id]+tmp;
for (RI x=ty[id]-tx[id]+1;x<=ty[id];++x)
{
int rem=true_pen-(x-1)*20;
if (240<=rem&&rem<=299)
{
tx[id]=x; ty[id]=rem; break;
}
}
} else state[id]=3,tx[id]=ty[id];
}
}
}
if (!flag) puts("No"); else
{
puts("Yes");
for (RI i=0;i<m;++i)
{
if (state[i]==1) printf("+ %d/%d\n",tx[i],ty[i]);
else if (state[i]==3) printf("- %d\n",tx[i]);
else if (state[i]==4) printf(".\n");
}
}
}
return 0;
}
E. Identical Parity
注意到这题的本质为,序列中每隔 \(k\) 个元素对应的位置上的数奇偶性相同,因此稍作分析可以把问题变为:
有 \(n\bmod k\) 个值为 \(\lfloor\frac{n}{k}\rfloor+1\) 的元素,以及 \(k-n\bmod k\) 个值为 \(\lfloor\frac{n}{k}\rfloor\) 的元素,要在其中找一个集合使得所有数的和为 \(\lfloor\frac{n}{2}\rfloor\)
这显然是一个二元一次方程,直接扩欧找出一个通解,然后判断是否存在一个满足个数限制的特解即可
Upt:今天写博客的时候发现由于 \(\lfloor\frac{n}{k}\rfloor+1,\lfloor\frac{n}{k}\rfloor\) 一定互质,因此扩欧这一步可以省略,通解是 trivial 的
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define LD long double
const LD eps = 1e-10;
int ex_gcd(int a, int b, int &x, int &y) {
if (0==b) {x = 1, y = 0; return a;}
int ret = ex_gcd(b, a%b, y, x);
y -= a/b*x;
return ret;
}
bool solve() {
int n, k;
cin >> n >> k;
if (n==k) return true;
if (1==k) return n==1;
int t = n/k, x = n%k;
int p0, q0;
ex_gcd(t+1, t, p0, q0);
// printf("t=%lld x=%lld p0=%lld q0=%lld\n", t, x, p0, q0);
int r = (n+1)/2;
int w1 = floor(min(1.0L*(x-r*p0)/t, 1.0L*r*q0/(t+1))+eps), w2 = ceil(max(1.0L*(r*q0+x-k)/(t+1), -1.0L*r*p0/t) - eps);
// printf("w1=%lld w2=%lld\n", w1, w2);
return w1 >= w2;
}
signed main() {
int T; cin >> T; while (T--) cout << (solve() ? "Yes\n" : "No\n");
return 0;
}
G. Quick Sort
感觉队友很牛啊,这题我题都没看就被秒了,后面看榜发现这题现场过的挺少的
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
int n, A[N], pos[N], ans=0;
void myswap(int a, int b) {
swap(pos[A[a]], pos[A[b]]);
swap(A[a], A[b]);
++ans;
}
int parti(int l, int r) {
int m = (l+r)/2;
int val = A[m];
int cntR = r-A[m], cntL = A[m]-l;
// printf("cntL=%d cntR=%d A[m]=%d\n", cntL, cntR, A[m]);
vector<int> vec;
if (cntL <= cntR) {
for (int j=l; j<=A[m]; ++j) vec.push_back(pos[j]);
sort(vec.begin(), vec.end());
for (int lp=0, rp=vec.size()-1, vv=l; lp<=rp; ++lp) {
while (lp<=rp && vv<vec[lp]) {
// printf("lp=%d rp=%d vv=%d vec[rp]=%d\n", lp, rp, vv, vec[rp]);
myswap(vv, vec[rp]);
--rp; ++vv;
}
++vv;
}
// printf("A:"); for (int i=1; i<=n; ++i) printf("%d ", A[i]); puts("");
return l+cntL;
} else {
for (int j=r; j>=A[m]; --j) vec.push_back(pos[j]);
sort(vec.begin(), vec.end());
for (int lp=0, rp=vec.size()-1, vv=r; lp<=rp; --rp) {
// printf("lp=%d rp=%d vv=%d vec[rp]=%d\n", lp, rp, vv, vec[rp]);
while (lp<=rp && vv>vec[rp]) {
// printf("1111\n");
myswap(vv, vec[lp]);
++lp; --vv;
}
--vv;
}
// printf("A:"); for (int i=1; i<=n; ++i) printf("%d ", A[i]); puts("");
// if (10 == n) while (1) {}
return r-cntR-1;
}
}
void quickSort(int l, int r) {
if (l>=0 && r>=0 && l<r) {
int p = parti(l, r);
// printf("quicksort l=%d r=%d p=%d\n", l, r, p);
quickSort(l, p);
quickSort(p+1, r);
}
}
int solve() {
ans = 0;
cin >> n;
for (int i=1; i<=n; ++i) cin >> A[i], pos[A[i]] = i;
quickSort(1, n);
return ans;
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int T; cin >> T; while (T--) cout << solve() << '\n';
return 0;
}
H. Set of Intervals
祁神发现这题 \(n\ge 4\) 的情况都可以转化为 \(n=4\) 的情况,即我们只关心左端点最小的两个区间和右端点最大的两个区间
因此这题的难点在于 \(n=3\) 的情形怎么分类讨论干净,徐神后面提出了一种转化为坐标系下直线下方矩形面积并中整点个数的问题,并讨论出一种相对好写的做法,可惜没时间写完了
徐神赛后宣布对此题负责
J. Skills
一个暴力的做法就是令 \(f_{i,j,k}\) 表示三种技能上一次学习的时间,若为 \(0\) 则认为从没学习过
首先要注意到一个关键性质:如果一个技能已经开始学习了,那么它的值后续一定不可能再被减到零,否则不如一开始就不学该技能
因此直接按上面的方法 DP 就不需要额外维护其它信息,不过这样状态数是 \(O(n^3)\) 的,还是无法通过
再进一步深挖上面的性质,假设当前是第 \(t\) 天,学习了技能一,那么很显然其它两个技能上一次学习的时间要么为 \(0\)(即从未学过),要么满足学习时间 \(\ge t-O(\sqrt {a_{i,j}})\)(否则上次学了等于白学)
因此实际上有用的状态大概是 \(O(n\times (\sqrt{a_i,j})^2)=O(n\times a_{i,j})\) 级别的,大力 DP 即可通过
下面的代码是第一版写的,用了 gp_hash_table 存状态,结果因为这题对常数要求比较苛刻,交上去 T 了
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#define RI register int
#define CI const int&
using namespace std;
using namespace __gnu_pbds;
const int RANDOM=chrono::high_resolution_clock::now().time_since_epoch().count();
struct chash
{
int operator()(CI x) const { return x^RANDOM; }
};
typedef gp_hash_table <int,int,chash> hash_t;
// hash_t = map <int,int>
const int N=1005,INF=1e9;
int t,n;
inline int encode(CI x,CI y,CI z) // 0<=x,y,z<=1000
{
return x*1001*1001+y*1001+z;
}
inline tuple <int,int,int> decode(CI mask)
{
return {mask/1001/1001,mask/1001%1001,mask%1001};
}
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
hash_t f; f[encode(0,0,0)]=0;
for (RI i=1;i<=n;++i)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
hash_t nf;
for (auto [mask,val]:f)
{
auto [a,b,c]=decode(mask);
// printf("(%d,%d,%d) %d\n",a,b,c,val);
if (a>0&&i-a>200) continue;
if (b>0&&i-b>200) continue;
if (c>0&&i-c>200) continue;
nf[encode(i,b,c)]=max(nf[encode(i,b,c)],x+val+(b?b-i:0)+(c?c-i:0));
nf[encode(a,i,c)]=max(nf[encode(a,i,c)],y+val+(a?a-i:0)+(c?c-i:0));
nf[encode(a,b,i)]=max(nf[encode(a,b,i)],z+val+(a?a-i:0)+(b?b-i:0));
}
swap(f,nf);
}
int ans=0;
for (auto [mask,val]:f) ans=max(ans,val);
printf("%d\n",ans);
}
return 0;
}
于是后面为了卡常就把上面优美简洁的代码改成了下面这个丑陋的版本,但时间快了不是一点半点:
#include<bits/stdc++.h>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005,INF=1e9;
int t,n,A[N][N],B[N][N],C[N][N],TA[N][N],TB[N][N],TC[N][N];
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
for (RI i=0;i<=n;++i) for (RI j=0;j<=n;++j)
A[i][j]=B[i][j]=C[i][j]=-INF;
A[0][0]=B[0][0]=C[0][0]=0;
for (RI i=1;i<=n;++i)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
for (RI b=max(1,i-200);b<=i;++b)
{
for (RI c=max(1,i-200);c<=i;++c)
if (b!=c) TA[b][c]=-INF;
TA[b][0]=-INF;
}
for (RI c=max(1,i-200);c<=i;++c)
TA[0][c]=-INF; TA[0][0]=-INF;
for (RI a=max(1,i-200);a<=i;++a)
{
for (RI c=max(1,i-200);c<=i;++c)
if (a!=c) TB[a][c]=-INF;
TB[a][0]=-INF;
}
for (RI c=max(1,i-200);c<=i;++c)
TB[0][c]=-INF; TB[0][0]=-INF;
for (RI a=max(1,i-200);a<=i;++a)
{
for (RI b=max(1,i-200);b<=i;++b)
if (a!=b) TC[a][b]=-INF;
TC[a][0]=-INF;
}
for (RI b=max(1,i-200);b<=i;++b)
TC[0][b]=-INF; TC[0][0]=-INF;
auto upt=[&](CI a,CI b,CI c,CI val)
{
TA[b][c]=max(TA[b][c],x+val+(b?b-i:0)+(c?c-i:0));
TB[a][c]=max(TB[a][c],y+val+(a?a-i:0)+(c?c-i:0));
TC[a][b]=max(TC[a][b],z+val+(a?a-i:0)+(b?b-i:0));
return;
};
for (RI b=max(1,i-201);b<=i-1;++b)
{
for (RI c=max(1,i-201);c<=i-1;++c)
if (b!=c) upt(i-1,b,c,A[b][c]);
upt(i-1,b,0,A[b][0]);
}
for (RI c=max(1,i-201);c<=i-1;++c)
upt(i-1,0,c,A[0][c]); upt(i-1,0,0,A[0][0]);
for (RI a=max(1,i-201);a<=i-1;++a)
{
for (RI c=max(1,i-201);c<=i-1;++c)
if (a!=c) upt(a,i-1,c,B[a][c]);
upt(a,i-1,0,B[a][0]);
}
for (RI c=max(1,i-201);c<=i-1;++c)
upt(0,i-1,c,B[0][c]); upt(0,i-1,0,B[0][0]);
for (RI a=max(1,i-201);a<=i-1;++a)
{
for (RI b=max(1,i-201);b<=i-1;++b)
if (a!=b) upt(a,b,i-1,C[a][b]);
upt(a,0,i-1,C[a][0]);
}
for (RI b=max(1,i-201);b<=i-1;++b)
upt(0,b,i-1,C[0][b]); upt(0,0,i-1,C[0][0]);
for (RI b=max(1,i-200);b<=i;++b)
{
for (RI c=max(1,i-200);c<=i;++c)
if (b!=c) A[b][c]=TA[b][c];
A[b][0]=TA[b][0];
}
for (RI c=max(1,i-200);c<=i;++c)
A[0][c]=TA[0][c]; A[0][0]=TA[0][0];
for (RI a=max(1,i-200);a<=i;++a)
{
for (RI c=max(1,i-200);c<=i;++c)
if (a!=c) B[a][c]=TB[a][c];
B[a][0]=TB[a][0];
}
for (RI c=max(1,i-200);c<=i;++c)
B[0][c]=TB[0][c]; B[0][0]=TB[0][0];
for (RI a=max(1,i-200);a<=i;++a)
{
for (RI b=max(1,i-200);b<=i;++b)
if (a!=b) C[a][b]=TC[a][b];
C[a][0]=TC[a][0];
}
for (RI b=max(1,i-200);b<=i;++b)
C[0][b]=TC[0][b]; C[0][0]=TC[0][0];
}
int ans=0;
for (RI b=max(1,n-200);b<=n;++b)
{
for (RI c=max(1,n-200);c<=n;++c)
if (b!=c) ans=max(ans,A[b][c]);
ans=max(ans,A[b][0]);
}
for (RI c=max(1,n-200);c<=n;++c)
ans=max(ans,A[0][c]); ans=max(ans,A[0][0]);
for (RI a=max(1,n-200);a<=n;++a)
{
for (RI c=max(1,n-200);c<=n;++c)
if (a!=c) ans=max(ans,B[a][c]);
ans=max(ans,B[a][0]);
}
for (RI c=max(1,n-200);c<=n;++c)
ans=max(ans,B[0][c]); ans=max(ans,B[0][0]);
for (RI a=max(1,n-200);a<=n;++a)
{
for (RI b=max(1,n-200);b<=n;++b)
if (a!=b) ans=max(ans,C[a][b]);
ans=max(ans,C[a][0]);
}
for (RI b=max(1,n-200);b<=n;++b)
ans=max(ans,C[0][b]); ans=max(ans,C[0][0]);
printf("%d\n",ans);
}
return 0;
}
K. Stack Sort
徐神开场写的签,我题目都没看
#include <bits/stdc++.h>
void work() {
int n; std::cin >> n;
std::vector<bool> isTop(n, false);
int ans = 0;
for(int i = 0, a; i < n; ++i) {
std::cin >> a; a -= 1;
if(a == n - 1 || !isTop[a + 1]) ans += 1;
else if(a < n - 1) isTop[a + 1] = false;
isTop[a] = true;
}
std::cout << ans << char(10);
}
int main() {
std::ios::sync_with_stdio(false);
int T; std::cin >> T;
while(T--) work();
return 0;
}
M. Best Carry Player
也是徐神开场写的签,我同样没看题
#include <bits/stdc++.h>
int ans = 0;
int cur[20];
void work() {
int n; std::cin >> n;
ans = 0;
memset(cur, 0, sizeof cur);
for(int i = 0, a; i < n; ++i) {
std::cin >> a;
int w = 0;
for(int j = 0; a || w; j += 1, a /= 10) {
cur[j] += a % 10 + w;
w = cur[j] / 10;
ans += (w > 0);
cur[j] %= 10;
}
}
std::cout << ans << char(10);
return ;
}
int main() {
std::ios::sync_with_stdio(false);
int T; std::cin >> T;
while(T--) work();
return 0;
}
Postscript
这周末就要打这赛季第一场 Regional ICPC 南京了,没想到这么快又回到了牢闪的福地南京,这次能做到吗

浙公网安备 33010602011771号