The 2024 ICPC Kunming Invitational Contest
Preface
周五去昆明特意买了动车,就为了在车上也能 VP 一场
找了半天不知道打什么,最后决定去昆明不如把今年的昆明邀请赛补一下,遂开了这场
前期可以说还是十分顺遂的,2h15min 的时候就过了 7 个题,其中还早早地过了后期题 H
但后面被 J 和 L 小卡了一手,最后通过换题成功把两个题都过了,然后 Rush F 失败,后面发现 F 好像有点简单
A. Two-star Contest
签到模拟题,祁神写的我题目都没看
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 4e5+5;
int n, m, k, s[N], rk[N];
vector<int> vec[N];
void solve() {
cin >> n >> m >> k;
for (int i=1; i<=n; ++i) {
cin >> s[i], rk[i] = i;
vec[i].clear(), vec[i].reserve(m);
for (int j=0; j<m; ++j) {
int a; cin >> a; vec[i].push_back(a);
}
}
sort(rk+1, rk+n+1, [&](int a, int b) {return s[a] < s[b];});
int pos = n+1;
for (int i=2; i<=n; ++i) if (s[rk[i]] > s[rk[i-1]]) {pos = i; break;}
int curval = 0, nxtval = 0;
for (int x=1; x<pos; ++x) {
auto &vecx = vec[rk[x]];
int ret = 0;
for (int i=0; i<m; ++i) {
if (-1==vecx[i]) vecx[i]=0;
else ret += vecx[i];
}
nxtval = max(ret, nxtval);
}
bool ok = true;
for (int i=pos; i<=n; ++i) {
if (s[rk[i]] > s[rk[i-1]]) curval = nxtval+1, ++nxtval;
auto &veci = vec[rk[i]];
int ret = 0;
for (int j=0; j<m; ++j) if (veci[j]>=0) ret += veci[j];
// printf("i=%lld ret=%lld\n", i, ret);
if (ret >= curval) {
nxtval = max(ret, nxtval);
for (int j=0; j<m; ++j) if (veci[j]<0) veci[j]=0;
} else {
int tmp = curval-ret;
for (int j=0; j<m; ++j) if (veci[j]<0) {
veci[j] = min(k, tmp),
tmp -= min(tmp, veci[j]);
}
if (tmp > 0) {
ok=false;
break;
}
}
}
if (!ok) cout << "No\n";
else {
cout << "Yes\n";
for (int i=1; i<=n; ++i) {
for (int x : vec[i]) cout << x << ' ';
cout << endl;
}
}
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int t; cin >> t; while (t--) solve();
return 0;
}
B. Gold Medal
签到,先贪心地把每个数补成 \(k\) 的倍数,多余部分直接计算即可
#include<cstdio>
#include<iostream>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
const int N=105;
int t,n,k,rem[N],m; pair <int,int> a[N];
signed main()
{
for (scanf("%lld",&t);t;--t)
{
scanf("%lld%lld",&n,&k);
for (RI i=1;i<=n;++i)
{
scanf("%lld",&a[i].se);
a[i].fi=(a[i].se+k-1)/k*k-a[i].se;
}
sort(a+1,a+n+1);
scanf("%lld",&m);
for (RI i=1;i<=n&&m>0;++i)
if (a[i].fi<=m) m-=a[i].fi,a[i].se+=a[i].fi; else a[i].se+=m,m=0;
int ans=0; if (m>0) ans+=m/k;
for (RI i=1;i<=n;++i) ans+=a[i].se/k;
printf("%lld\n",ans);
}
return 0;
}
E. Relearn through Review
其实很早的时候就猜出做法了,不过由于不确定正确性就一直没写
首先不难发现如果我们修改 \([l,r]\) 区间,则答案由三部分构成,即不变的前缀/后缀,以及中间集体加 \(k\) 的一段
注意到当前后缀的贡献不变时,中间部分的长度肯定是越短越好,而我们知道前后缀 \(\gcd\) 的值只有 \(\log a_i\) 段
因此把所有的前后缀变化的分界点拉出来暴力枚举下 \([l,r]\) 即可,中间部分的贡献可以用 ST 表预先维护
复杂度 \(O(n\log n\log a_i+\log^3 a_i)\),注意可以一次都不操作
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=300005;
int t,n,k,a[N],pre[N],suf[N],lg[N],f[N][20];
inline int query(CI l,CI r)
{
int k=lg[r-l+1]; return __gcd(f[l][k],f[r-(1<<k)+1][k]);
}
signed main()
{
for (scanf("%lld",&t);t;--t)
{
scanf("%lld%lld",&n,&k);
for (RI i=1;i<=n;++i) scanf("%lld",&a[i]);
pre[0]=suf[n+1]=0;
for (RI i=1;i<=n;++i) pre[i]=__gcd(pre[i-1],a[i]);
for (RI i=n;i>=1;--i) suf[i]=__gcd(suf[i+1],a[i]);
for (RI i=1;i<=n;++i) f[i][0]=a[i]+k;
lg[0]=-1; for (RI i=1;i<=n;++i) lg[i]=lg[i>>1]+1;
for (RI j=1;j<20;++j)
for (RI i=1;i+(1<<j)-1<=n;++i)
f[i][j]=__gcd(f[i][j-1],f[i+(1<<j-1)][j-1]);
vector <int> vec;
for (RI i=1;i<=n;++i)
{
if (pre[i]!=pre[i-1])
{
if (i-1>=1) vec.push_back(i-1);
vec.push_back(i);
}
if (suf[i]!=suf[i+1])
{
if (i+1<=n) vec.push_back(i+1);
vec.push_back(i);
}
}
sort(vec.begin(),vec.end());
vec.erase(unique(vec.begin(),vec.end()),vec.end());
int ans=pre[n];
for (RI i=0;i<vec.size();++i)
for (RI j=i;j<vec.size();++j)
{
int l=vec[i],r=vec[j];
int cur=__gcd(__gcd(pre[l-1],suf[r+1]),query(l,r));
ans=max(ans,cur);
}
printf("%lld\n",ans);
}
return 0;
}
F. Collect the Coins
感觉想到了就很简单的一个题啊,但我们队有人在想图论几何建模,有人在贪心,怎么回事呢
首先一眼二分答案 \(v\),考虑递推检验,令 \([l_i,r_i]\) 表示在处理完第 \(i\) 枚硬币后,一个机器人在 \(c_i\),另一个机器人可以的取值在 \([l_i,r_i]\) 内,初始时 \([l_1,r_1]=(-\infty,\infty)\)
考虑新加入一枚硬币 \(i+1\) 的影响,令 \(d=(t_{i+1}-t_i)\times v\):
- 若 \(c_{i+1}\in[c_i-d,c_i+d]\),则之前在 \(c_i\) 的机器人可以收集到硬币,此时另一个机器人可能的取值变为 \([l_i-d,r_i+d]\);
- 若 \(c_{i+1}\in[l_i-d,r_i+d]\),则之前不在 \(c_i\) 的机器人可以收集到硬币,此时另一个机器人可能的取值变为 \([c_i-d,c_i+d]\);
注意到如果以上两种 Case 都不成立则一定无解,如果成立其一就直接转移即可
当同时成立时由于两个区间都包含 \(c_{i+1}\),因此这两个区间一定相交,可以合并成一个大区间
复杂度 \(O(n\log c_i)\)
#include<cstdio>
#include<iostream>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,INF=1e18;
struct itv
{
int l,r;
inline itv(CI L=0,CI R=0)
{
l=L; r=R;
}
inline bool is_in(CI x)
{
return l<=x&&x<=r;
}
friend inline itv operator + (const itv& A,const itv& B)
{
return itv(min(A.l,B.l),max(A.r,B.r));
}
};
int T,n,t[N],c[N];
inline bool check(CI v)
{
itv cur(-INF,INF);
for (RI i=2;i<=n;++i)
{
int dlt=(t[i]-t[i-1])*v;
itv np=itv(cur.l-dlt,cur.r+dlt),nq=itv(c[i-1]-dlt,c[i-1]+dlt);
bool p=nq.is_in(c[i]),q=np.is_in(c[i]);
if (p&&q) { cur=np+nq; continue; }
if (p) { cur=np; continue; }
if (q) { cur=nq; continue; }
return 0;
}
return 1;
}
signed main()
{
for (scanf("%lld",&T);T;--T)
{
scanf("%lld",&n);
for (RI i=1;i<=n;++i) scanf("%lld%lld",&t[i],&c[i]);
int l=0,r=1e9,mid,ret=-1; while (l<=r)
if (check(mid=l+r>>1)) ret=mid,r=mid-1; else l=mid+1;
if (ret==-1) puts("-1"); else printf("%lld\n",ret);
}
return 0;
}
G. Be Positive
签到,根据经典结论当 \(n=1\or 4\mid n\) 时一定无解,因为此时所有数异或起来都等于 \(0\)
否则按照四个数一组,交换相邻两组相邻的元素即可构造出字典序最小的解
#include<cstdio>
#include<iostream>
#include<cassert>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5;
int t,n,a[N];
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
if (n==1||n%4==0) { puts("impossible"); continue; }
for (RI i=0;i<n;++i) a[i]=i; swap(a[0],a[1]);
for (RI i=3;i+1<n;i+=4) swap(a[i],a[i+1]);
int xsum=0; for (RI i=0;i<n;++i)
xsum^=a[i],assert(xsum!=0);
for (RI i=0;i<n;++i) printf("%d%c",a[i]," \n"[i==n-1]);
}
return 0;
}
H. Subarray
感觉很典的一个题啊,尤其是处理贡献的卷积部分好像和南京的 I 一模一样
考虑从大到小枚举每个数作为最大值的贡献,此时很容易用单调栈算出此时每个数左边/右边有多少个合法的端点选择,不妨记为数组 \(\{a_i\},\{b_i\}\),但要注意被隔断的情况
此时不难发现要计算贡献的话就是枚举所有的 \(i\in[1,k],j\in[i,k]\),然后 \(a_i\times b_j\rightarrow c_{j-i+1}\)
这个式子直接把 \(\{a_i\}\) reverse 一下然后和 \(\{b_i\}\) 做卷积,然后平移一下就能快速计算贡献了
然后这题就做完了,复杂度 \(O(n\log n)\)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=400005,mod=998244353;
int t,n,a[N],rst[N],l[N],r[N],stk[N],top,ans[N]; vector <int> pos[N];
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline void dec(int& x,CI y)
{
if ((x-=y)<0) x+=mod;
}
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
namespace Poly
{
int lim,p,rev[N<<2];
inline void init(CI n)
{
for (lim=1,p=0;lim<=n;lim<<=1,++p);
for (RI i=0;i<lim;++i) rev[i]=(rev[i>>1]>>1)|((i&1)<<p-1);
}
inline void NTT(int *f,CI opt)
{
RI i; for (i=0;i<lim;++i) if (i<rev[i]) swap(f[i],f[rev[i]]);
for (i=1;i<lim;i<<=1)
{
int m=i<<1,D=quick_pow(3,opt==1?(mod-1)/m:mod-1-(mod-1)/m);
for (RI j=0;j<lim;j+=m)
{
int W=1; for (RI k=0;k<i;++k,W=1LL*W*D%mod)
{
int x=f[j+k],y=1LL*f[i+j+k]*W%mod;
f[j+k]=f[i+j+k]=x; inc(f[j+k],y); dec(f[i+j+k],y);
}
}
}
if (!~opt)
{
int Inv=quick_pow(lim); for (i=0;i<lim;++i) f[i]=1LL*f[i]*Inv%mod;
}
}
};
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
for (RI i=1;i<=n;++i) scanf("%d",&a[i]),rst[i]=a[i];
sort(rst+1,rst+n+1); int m=unique(rst+1,rst+n+1)-rst-1;
for (RI i=1;i<=n;++i) a[i]=lower_bound(rst+1,rst+m+1,a[i])-rst;
for (RI i=1;i<=m;++i) pos[i].clear();
for (RI i=1;i<=n;++i) pos[a[i]].push_back(i);
stk[top=0]=0;
for (RI i=1;i<=n;++i)
{
while (top&&a[stk[top]]<a[i]) --top;
l[i]=stk[top]; stk[++top]=i;
}
stk[top=0]=n+1;
for (RI i=n;i>=1;--i)
{
while (top&&a[stk[top]]<a[i]) --top;
r[i]=stk[top]; stk[++top]=i;
}
for (RI i=1;i<=m;++i)
{
auto solve=[&](vector <int>& vec)
{
static int A[N<<2],B[N<<2];
int sz=(int)vec.size();
for (RI i=0;i<sz;++i)
{
A[sz-i]=vec[i]-l[vec[i]];
B[i]=r[vec[i]]-vec[i];
}
Poly::init(2*sz); Poly::NTT(A,1); Poly::NTT(B,1);
for (RI i=0;i<Poly::lim;++i) A[i]=1LL*A[i]*B[i]%mod;
Poly::NTT(A,-1);
for (RI i=1;i<=sz;++i) inc(ans[i],A[i+sz-1]);
for (RI i=0;i<Poly::lim;++i) A[i]=B[i]=0;
};
vector <int> vec; vec.push_back(pos[i][0]);
for (RI j=1;j<pos[i].size();++j)
{
int p=vec.back(),q=pos[i][j];
if (r[p]<q) solve(vec),vec.clear();
vec.push_back(q);
}
solve(vec);
}
// for (RI i=1;i<=n;++i) printf("%d%c",ans[i]," \n"[i==n]);
int ret=0;
for (RI i=1;i<=n;++i) inc(ret,1LL*i*ans[i]%mod*ans[i]%mod),ans[i]=0;
printf("%d\n",ret);
}
return 0;
}
I. Left Shifting 2
徐神开场写的,应该也是个签
#include <iostream>
#include <format>
int main() {
int T; std::cin >> T; while(T--) {
std::string s; std::cin >> s;
int ans = 0, t = 0, n = s.size();
bool flag = true;
for(int l = 0, r; l < n; l = r) {
r = l + 1;
while(r < n && s[r] == s[l]) r++;
if(r - l == n) {
flag = false;
ans = n / 2;
break;
}
if((l == 0 || r == n) && s[0] == s[n - 1])
continue;
ans += (r - l) / 2;
if((r - l) % 2 == 0) t = 1;
}
if(flag && s[0] == s[n - 1]) {
int l, r;
for(l = 0; l < n && s[l] == s[0]; l++);
for(r = n - 1; r >= 0 && s[r] == s[0]; r--);
ans += (l + (n - 1) - r) / 2;
if((l + n - r - 1) % 2 == 0) t = 1;
}
std::cout << std::max(0, ans - t) << char(10);
}
return 0;
}
J. The Quest for El Dorado
我和祁神讨论了半天,徐神在 L 坐牢;最后一波交换后我去写 L,徐神来写 J,成功双开双过
徐神的做法好像是基于欠着贡献先走到某个点,然后对某个颜色开堆单独维护最优的转移方案
虽然我基本没咋听懂,但你就说过没过题吧
#include <iostream>
#include <queue>
using llsi = long long signed int;
using pii = std::pair<int, int>;
using tuple = std::tuple<int, int, int>;
template<typename T>
using less_pq = std::priority_queue<T, std::vector<T>, std::greater<T>>;
void work() {
int n, m, k;
std::cin >> n >> m >> k;
std::vector<std::vector<tuple>> out(n);
for(int i = 0, u, v, c, l; i < m; ++i) {
std::cin >> u >> v >> c >> l;
u--, v--, c--;
out[u].emplace_back(v, c, l);
out[v].emplace_back(u, c, l);
}
std::vector<pii> dis(n, {1e9, 0});
std::vector<bool> vis(n, false);
std::vector<less_pq<std::pair<int, int>>> hang(m);
less_pq<std::pair<pii, int>> cur;
for(int i = 0; i < m; ++i) hang[i].push({0, 0});
for(int i = 1, a, b; i <= k; ++i) {
std::cin >> a >> b; a--;
// if(hang[a].size()) std::cerr << "hang[" << a << "].top = [" << hang[a].top().first << ", " << hang[a].top().second << "]\n";
while(hang[a].size() && hang[a].top().first <= b) {
cur.push({{i, hang[a].top().first}, hang[a].top().second});
hang[a].pop();
}
while(cur.size()) {
auto [cur_dis, current] = cur.top(); cur.pop();
if(vis[current]) continue;
// std::cerr << "cur, i, cur_dis = " << current + 1 << ", " << i << ", " << cur_dis.second << char(10);
vis[current] = 1;
for(auto [out, color, length]: out[current]) {
if(vis[out]) continue;
// std::cerr << "out, color, a, i, cur_dis.second + length = " << out + 1 << ", " << color << ", " << a << ", " << i << ", " << cur_dis.second + length << char(10);
if(color != a) {
hang[color].push({length, out});
continue;
}
if(cur_dis.second + length <= b && pii(i, cur_dis.second + length) < dis[out]) {
dis[out] = pii(i, cur_dis.second + length);
cur.push({dis[out], out});
} else {
hang[a].push({length, out});
continue;
}
}
}
}
// std::cerr << n << " " << m << " " << k << char(10);
for(int i = 0; i < n; ++i) std::cout << int(vis[i]);
std::cout << char(10);
}
int main() {
std::ios::sync_with_stdio(false);
int T; std::cin >> T; while(T--) work();
return 0;
}
L. Trails
刚开始徐神写了个基于差分的做法,然后我画了几个图找了个反例把做法叉了,随后才意识到这题和 LIS 很像,遂用经典的二分+单调 DP 数组解决
考虑用扫描线的思路,维护一条平行于 \(x\) 轴的线,可以先假设全局一条斜线都没有算一个贡献,最后再把斜线带来的影响减去
不妨设 \(f_i\) 表示经过 \(i\) 条斜线后最小的 \(y\) 坐标是多少,不难发现 \(f\) 单调上升,且最后需要减去的影响就是 \(\sum(q-f_i)\)
要维护 \(f_i\),只需要将所有斜线按照 \(x\) 从小到大,\(y\) 从大到小排序,每次转移的时候找到最大的 \(r\) 使得 \(f_r\le y\),并将 \(f_{r+1}\) 修改为 \(y+1\) 即可,代码十分好写
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
using llsi = long long signed int;
void work() {
int n;
llsi p, q;
std::cin >> n >> p >> q;
std::vector<std::pair<int, int>> hkr;
for(int i = 0, x, y; i < n; ++i) {
std::cin >> x >> y;
if(x >= p || y >= q) continue;
hkr.emplace_back(x, y);
}
auto cmp=[&](const std::pair<int, int>& A,const std::pair<int, int>& B)
{
return A.first!=B.first?A.first<B.first:A.second>B.second;
};
std::sort(hkr.begin(), hkr.end(), cmp);
llsi ue = 0, ans = (q + 1) * q / 2 * (p + 1) + (p + 1) * p / 2 * (q + 1);
std::vector <int> f;
f.push_back(0);
int l = 0;
for(auto [x, y]: hkr) {
ans -= (x - l) * ue;
l = x;
if (y>=f.back())
{
f.push_back(y+1);
ue+=q-y;
} else
{
int p=upper_bound(f.begin(),f.end(),y)-f.begin();
if (p>=0&&p<f.size())
{
ue+=f[p]-(y+1);
f[p]=y+1;
}
}
}
ans -= (p - l) * ue;
std::cout << ans << char(10);
}
int main() {
std::ios::sync_with_stdio(false);
int T; std::cin >> T; while(T--) work();
return 0;
}
M. Italian Cuisine
挺 trivial 的一个几何,开场 1h 左右就被祁神切了,我题意都不知道
#include<bits/stdc++.h>
using namespace std;
#define int long long
using i128 = __int128;
i128 sqr(i128 x) {return x*x;}
struct Pt {
int x, y;
int crs(const Pt &b) const {return x*b.y - y*b.x;}
Pt operator-(const Pt &b) const {return Pt{x-b.x, y-b.y};}
int len2() const {return x*x + y*y;}
};
int cross(const Pt &p, const Pt &a, const Pt &b) {return (a-p).crs(b-p);}
const int N = 2e5+5;
i128 sum[N];
int solve() {
int n; cin >> n;
vector<Pt> pt; pt.resize(2*n+1);
int x0, y0, r0; cin >> x0 >> y0 >> r0;
Pt c = Pt{x0, y0};
for (int i=0; i<n; ++i) {
int x, y; cin >> x >> y;
pt[i] = pt[n+i] = Pt{x, y};
}
pt[2*n] = pt[0];
for (int i=1; i<=2*n; ++i) sum[i] = sum[i-1] + pt[i-1].crs(pt[i]);
auto check = [&](int x, int y) {
int res = cross(pt[x], pt[y], c);
return (res > 0) && (sqr(res) >= sqr(r0) * (pt[x]-pt[y]).len2());
};
int ans = 0;
for (int i=0, j=1; i<n; ++i, j=max(j, i+1)) {
while (j<=2*n && check(i, j)) {
// printf("i=%lld j=%lld\n", i, j);
ans = max((i128)ans, sum[j]-sum[i]+pt[j].crs(pt[i]));
++j;
}
}
return ans;
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int T; cin >> T; while (T--) cout << solve() << '\n';
return 0;
}
Postscript
感觉真得打 SUA 出题的赛站,各种知识点的题目都有,不然遇到不对胃口的场直接等死就完了

浙公网安备 33010602011771号