The 2023 ICPC Asia Nanjing Regional Contest (The 2nd Universal Cup. Stage 11: Nanjing)
Preface
每周末惯例VP两场,这周可以把这个赛季剩下的两场ICPC(南京和西安)都补了,之后有空就接着VP这个赛季的CCPC
这场虽然开局因为硬做袋鼠题导致开题顺序有点问题,但好在摸了半天还是较快地摸出来了
中间虽然一度出现F读假题导致完全想+写了个错误做法,以及签到G卡了好久没写出来
但好在后面都是顺利解决,然后medium的几个题出的也较为顺利,最后也是7题罚时中游苟进Au区
值得一提的是难得有中文题面结果我们队疯狂看错题(徐神J题也看错了),评价是不会中文导致的
A. Cool, It’s Yesterday Four Times More
南京站惯例袋鼠题,受不了一点开局直接不管其它题一头攒死
这题的做法其实不难,我们可以直接用四元组\((x_1,y_1,x_2,y_2)\)表示对于初始位置为\((x_1,y_1)\)的袋鼠,它最后能否干掉初始位置为\((x_2,y_2)\)的袋鼠
转移就考虑四种走路方向,刚开始很容易想到如果存在某个方向,使得\((x_1,y_1)\)还是走到合法的位置,并且\((x_2,y_2)\)走到了非法的位置,那么这个状态显然就是可以达成的
那么可以考虑用一个类似于记忆化搜索的东西来直接转移,但实际写一发后会发现有个很严重的问题
即转移过程中可能会出现从某个状态出发绕了一圈又回到了这个状态,然后由于当前状态的答案还没有确定,会直接死循环
而要解决这个问题也很简单,我们不妨先把可以相互转移的状态间连有向边,最后从所有必胜态出发,将能遍历到的所有点都置为必胜态即可
总复杂度\(O(n^2m^2)\)
#include<cstdio>
#include<iostream>
#include<vector>
#include<queue>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005,dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
int t,n,m,vis[N*N]; char a[N][N]; vector <int> v[N*N]; queue <int> q;
inline int trs(CI x,CI y)
{
return (x-1)*m+y;
}
inline int trs2(CI a,CI b,CI c,CI d)
{
return (trs(a,b)-1)*n*m+trs(c,d);
}
inline void link(CI x_1,CI y_1,CI x_2,CI y_2)
{
for (RI i=0;i<4;++i)
{
int tx_1=x_1+dx[i],ty_1=y_1+dy[i];
if (tx_1<1||tx_1>n||ty_1<1||ty_1>m) continue;
if (a[tx_1][ty_1]=='O') continue;
int tx_2=x_2+dx[i],ty_2=y_2+dy[i];
if (tx_2<1||tx_2>n||ty_2<1||ty_2>m||a[tx_2][ty_2]=='O')
{
q.push(trs2(x_1,y_1,x_2,y_2)); vis[trs2(x_1,y_1,x_2,y_2)]=1; continue;
}
v[trs2(tx_1,ty_1,tx_2,ty_2)].push_back(trs2(x_1,y_1,x_2,y_2));
}
}
int main()
{
//freopen("A.in","r",stdin);
for (scanf("%d",&t);t;--t)
{
RI x,y,i,j; for (scanf("%d%d",&n,&m),i=1;i<=n;++i) scanf("%s",a[i]+1);
q=queue <int>(); for (i=1;i<=n*n*m*m;++i) v[i].clear(),vis[i]=0;
int ans=0; for (x=1;x<=n;++x) for (y=1;y<=m;++y) if (a[x][y]=='.')
for (i=1;i<=n;++i) for (j=1;j<=m;++j)
if (a[i][j]=='.') if (x!=i||y!=j) link(x,y,i,j);
while (!q.empty())
{
int now=q.front(); q.pop();
for (auto to:v[now]) if (!vis[to]) vis[to]=1,q.push(to);
}
for (x=1;x<=n;++x) for (y=1;y<=m;++y) if (a[x][y]=='.')
{
bool flag=1; for (i=1;i<=n&&flag;++i) for (j=1;j<=m&&flag;++j)
if (a[i][j]=='.') if (x!=i||y!=j) if (vis[trs2(x,y,i,j)]==0) flag=0;
ans+=flag;
}
printf("%d\n",ans);
}
return 0;
}
B. Intersection over Union
到现在CF上也没人过的神之几何题,弃疗
C. Primitive Root
什么原神根数论题,我题目都没看就被徐神秒了
(额好像和原根一点关系没有是个顶针位运算题)
#include <bits/stdc++.h>
#include <functional>
using llsi = long long signed int;
void hkr(llsi p, llsi m, std::function<void(llsi, llsi)> callback) {
llsi cur = 0;
for(int i = 62; i >= 0; --i) {
if(p >> i & 1) {
if(m >> i & 1) {
callback(cur | (1ll << i), cur | (1ll << i + 1) - 1);
} else {
cur |= 1ll << i;
}
} else {
if(m >> i & 1) {
callback(cur, cur | (1ll << i) - 1);
cur |= 1ll << i;
} else {
}
}
}
if((cur ^ p) <= m) callback(cur, cur);
return ;
}
int main() {
// freopen("1.in", "r", stdin);
int T; std::cin >> T; while(T--) {
llsi p, m, ans = 0;
std::cin >> p >> m;
hkr(p - 1, m, [&](llsi l, llsi r) {
// std::cerr << "l, r = " << l << ", " << r << char(10);
l += p - 1; r += p - 1;
ans += r / p - (l - 1) / p;
});
std::cout << ans << char(10);
}
return 0;
}
D. Red Black Tree
唉原来大家都知道min+卷积的常用优化是在差分数组上做,看来还是我见识少了
首先很容易推出一个暴力DP做法,设\(f_{x,y}\)表示要当点\(x\)的子树合法,且\(x\)到其子树内所有叶节点的路径上黑点的数量都是\(y\)时,最少的修改次数
设\(g_{x,0/1}\)表示将点\(x\)变红/黑的代价,则有转移:
考虑最后点\(x\)的答案就是\(\min f_{x,y}\),直觉告诉我们\(f_{x,\cdot}\)感觉关于\(y\)显然是单峰的
这里就不具体证明了,详细的可以看官方题解,事实上如果你对闵可夫斯基和很熟悉的话就会知道凸函数的\(\min+\)卷积依然是凸函数,那么这个结论就显而易见了
而为了求凸序列的最值,我们还需要维护其差分数组,事实上两个凸函数的\(\min+\)卷积就等价于将它们的首项直接加起来,然后将差分数组归并排序(具体可以看这里)
考虑知道了这些后怎么计算答案,首先\(\sum_{z\in son(x)} f_{z,y-i}\)可以直接暴力计算,每次合并子树的时候保留深度较小的那边即可
因为我们知道长链剖分的时候,我们保留的都是深度最大的那边,这样的复杂度都才\(O(n)\),而这样做复杂度是一定比原来要小的
因此现在剩下的部分就是要在某个差分序列中插入一个\(\pm 1\)了,直接用multiset
维护差分序列已经足以通过此题,但事实上还有更好的做法
注意到我们每次插入的数都是\(\pm 1\),因此可以用如下的数据结构来维护一个有序序列
即对于负数开一个vector
,其中元素从小到大有序,这样每次插入\(-1\)就直接push_back
到最后即可
同理对于正数开一个vector
,其中元素从大到小有序,这样每次插入\(1\)就直接push_back
到最后即可
最后再特别维护一下\(0\)的个数,则可以完成\(O(1)\)插入\(\pm 1\)以及\(O(1)\)询问某个下标的值了,总复杂度\(O(n)\)
#include<cstdio>
#include<iostream>
#include<vector>
#include<utility>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,x,ans[N]; char s[N]; vector <int> v[N];
struct delta
{
vector <int> pos,neg; int zero,sum;
inline delta(void)
{
pos.clear(); neg.clear(); zero=sum=0;
}
inline delta(vector <int>& vec)
{
zero=sum=0; pos.clear(); neg.clear();
for (auto x:vec) if (x<0) neg.push_back(x),sum+=x;
else if (x==0) ++zero; else pos.push_back(x);
reverse(pos.begin(),pos.end());
}
inline int size(void)
{
return neg.size()+zero+pos.size();
}
inline int at(CI x)
{
if (x<neg.size()) return neg[x]; else
if (x<neg.size()+zero) return 0; else
return pos[pos.size()-1-(x-neg.size()-zero)];
}
inline void insert(CI x)
{
if (x==-1) neg.push_back(x),--sum; else pos.push_back(x);
}
};
inline pair <int,delta> DFS(CI now=1)
{
int st=0; delta d;
for (auto to:v[now])
{
auto [x,vec]=DFS(to); st+=x;
if (d.size()==0) d=move(vec); else
{
int lim=min(d.size(),vec.size());
vector <int> tmp(lim);
for (RI i=0;i<lim;++i) tmp[i]=d.at(i)+vec.at(i);
d=delta(tmp);
}
}
st+=s[now]-'0'; d.insert(s[now]=='0'?1:-1);
ans[now]=st+d.sum; return make_pair(st,move(d));
}
int main()
{
for (scanf("%d",&t);t;--t)
{
RI i; for (scanf("%d%s",&n,s+1),i=2;i<=n;++i) scanf("%d",&x),v[x].push_back(i);
for (DFS(),i=1;i<=n;++i) printf("%d%c",ans[i]," \n"[i==n]),v[i].clear();
}
return 0;
}
E. Extending Distance
题目都没看,做不来捏
F. Equivalent Rewriting
这题刚开始祁神看错题了然后我还想出了个很有趣的做法,结果后面写完才发现题读错了(不过还是有点用的这个idea可以留着之后出题)
这题做法也很简单,对于序列中的每个位置,不妨设在上面按序进行了\(\{a_1,a_2,\cdots,a_k\}\)这些操作,则不难发现最后这个位置上的值只和\(a_k\)有关
不难想到将每个操作看作一个点,则每次连边\(a_i\to a_k\ \ (i\in[1,k-1])\),这样对于给定的图的任意一个合法的拓扑序都是原问题的合法答案
为了避免讨论,有一种很好写的做法,注意到\(\{1,2,3,\cdots,n\}\)是合法的拓扑序中字典序最小的,因此我们找出图中字典序最大的拓扑序即可
总复杂度\(O(n\log n)\)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int t, n, m;
vector<int> pos[N];
vector<int> G[N]; int in[N];
int ans[N], tp=0;
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cin >> t;
while (t--){
cin >> n >> m;
for (int i=1; i<=m; ++i) pos[i].clear();
for (int i=1; i<=n; ++i) G[i].clear(), in[i]=0;
tp=0;
for (int i=1; i<=n; ++i){
int p; cin >> p;
for (int j=1; j<=p; ++j){
int a; cin >> a;
pos[a].push_back(i);
}
}
for (int i=1; i<=m; ++i){
int sz = pos[i].size();
for (int j=0; j<sz-1; ++j){
G[pos[i][j]].push_back(pos[i][sz-1]);
++in[pos[i][sz-1]];
}
}
priority_queue<int> Q;
for (int i=1; i<=n; ++i) if (!in[i]) Q.push(i);
while (!Q.empty()){
int x=Q.top(); Q.pop();
ans[++tp]=x;
for (int v : G[x]){
--in[v];
if (!in[v]) Q.push(v);
}
}
bool ok=false;
for (int i=1; i<=n; ++i) if (ans[i]!=i){ok=true; break;}
if (ok){
cout << "Yes\n";
for (int i=1; i<=n; ++i) cout << ans[i] << (n==i ? '\n' : ' ');
}else cout << "No\n";
}
return 0;
}
G. Knapsack
妈的刚开始脑抽了想着按照\(v_i\)排序,后面发现我犯病了显然是按照\(w_i\)排序才对,一直拖到2h的时候才过了这个签到题
不难发现将所有物品按照\(w_i\)从小到大排序,则显然存在一个分界点,使得做背包的物品都在某段前缀中,而零元购的物品都在对应的后缀中
前缀部分就用0/1背包推一下,后缀部分就经典地用堆维护若干个数中取\(k\)个最大的,总复杂度\(O(nW+n\log n)\)
#include<cstdio>
#include<iostream>
#include<queue>
#include<utility>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
typedef pair <int,int> pi;
const int N=10005;
int n,W,k,suf[N],f[N],ans; pi p[N];
signed main()
{
RI i,j; for (scanf("%lld%lld%lld",&n,&W,&k),i=1;i<=n;++i)
scanf("%lld%lld",&p[i].fi,&p[i].se); sort(p+1,p+n+1);
priority_queue <int,vector <int>,greater <int>> hp;
for (i=n;i>=n-k+1;--i) suf[i]=suf[i+1]+p[i].se,hp.push(p[i].se);
for (i=n-k;i>=1;--i) hp.push(p[i].se),suf[i]=suf[i+1]+p[i].se-hp.top(),hp.pop();
for (ans=suf[1],i=1;i<=n;++i)
{
for (j=W;j>=p[i].fi;--j) f[j]=max(f[j],f[j-p[i].fi]+p[i].se);
for (j=0;j<=W;++j) ans=max(ans,f[j]+suf[i+1]);
}
return printf("%lld",ans),0;
}
H. Puzzle: Question Mark
做不来,弃疗捏
I. Counter
签到,祁神一眼秒了我就没管
#include<bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
#define ft first
#define sd second
const int N = 1e5+5;
int t, n, m;
pii A[N];
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cin >> t;
while (t--){
cin >> n >> m;
A[0]=make_pair(0, 0);
for (int i=1; i<=m; ++i) cin >> A[i].ft >> A[i].sd;
sort(A, A+m+1);
bool ok=true;
for (int i=1; i<=m; ++i){
if ((A[i].ft-A[i-1].ft==A[i].sd-A[i-1].sd) || A[i].ft-A[i].sd>A[i-1].ft) continue;
else{ok=false; break;}
}
cout << (ok ? "Yes\n" : "No\n");
}
return 0;
}
J. Suffix Structure
徐神看反了字串的要求导致把不可做题辨认为了可做题,但也无伤大雅反正后面1h我们也没有会做的题了
K. Grand Finale
华·丽·收·场!
比赛的时候和祁神讨论了好多Reduction Rule然后感觉细节爆多,后面看了眼题解发现要写好几个DP,直接开摆
L. Elevator
贪心题,注意到如果只有体积为\(2\)的物品那么直接每次贪心装高度最大的那些一定最优
现在加上体积为\(1\)的物品也很简单,我们依照上面的贪心策略把体积为\(1\)的物品都合并为体积为\(2\)的物品即可
实现的时候要注意一些奇奇怪怪的细节,但思路总体还是很自然的
#include<bits/stdc++.h>
using namespace std;
#define int long long
using pii = pair<int, int>;
#define ft first
#define sd second
int t, n, k;
vector<pii> A[2];
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cin >> t;
while (t--){
cin >> n >> k;
A[0].clear(), A[1].clear();
for (int i=1; i<=n; ++i){
int c, w, f; cin >> c >> w >> f;
A[w-1].emplace_back(f, c);
}
sort(A[0].begin(), A[0].end());
int carry=1;
for (int i=(int)A[0].size()-1; i>=0; --i){
auto [f, c] = A[0][i];
if (c%2==0) A[1].emplace_back(f, c/2);
else{
A[1].emplace_back(f, (c+carry)/2);
carry^=1;
}
}
sort(A[1].begin(), A[1].end());
k/=2;
int lst=0;
int ans=0;
for (int i=A[1].size()-1; i>=0; --i){
auto [f, c] = A[1][i];
// printf("f=%lld c=%lld i=%lld\n", f, c, i);
if (c<=lst){
lst -= c;
continue;
}else{
c -= lst;
ans += (c+k-1)/k * f;
if (c%k>0) lst = k - c%k;
else lst=0;
}
}
cout << ans << '\n';
}
return 0;
}
M. Trapping Rain Water
经典考验码力的DS题,这次还算比较手稳写+调了四五十分钟就过了
首先题目中好心地给出了你答案的计算式子,考虑先将\(\sum -a_i\)单独拆出来,只需考虑前面的\(\sum \min(f_i,g_i)\)如何处理
很容易发现\(f_i\)是单调不降的,同时\(g_i\)是单调不升的,这就意味着对于\(\min(f_i,g_i)\),一定存在某个分界点\(p\),使得\(i\in[1,p]\)时值都取\(f_i\),\(i\in[p+1,n]\)时值都取\(g_i\)
而找这个分界点显然可以直接搞个线段树维护两个值,然后在线段树上二分,最后要得到答案还要分别搞个区间求和
现在考虑修改,注意到修改只有升高而不会降低,因此以\(f_i\)为例,此时有个很好的性质就是它只会将一段区间的\(f_i\)的值全部赋值为\(a_x+y\)
而要找到这样的区间也很显然,直接在线段树上二分找到\(x\)右侧第一个\(\ge a_x+y\)的下标即可,对于\(g_i\)的修改同理
然后只要爬上去把上面提到的操作都写完即可,总复杂度\(O(n\log n)\),但常数较大
#include<cstdio>
#include<iostream>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,a[N],pre[N],suf[N],q,x,y,sum;
class Segment_Tree
{
private:
struct segment
{
int sum_pre,sum_suf,tag_pre,tag_suf;
int mx_pre,mn_pre,mx_suf,mn_suf,len;
inline segment(void)
{
sum_pre=sum_suf=tag_pre=tag_suf=0;
mx_pre=mn_pre=mx_suf=mn_suf=0;
}
}O[N<<2];
inline void pushup(CI now)
{
O[now].sum_pre=O[now<<1].sum_pre+O[now<<1|1].sum_pre;
O[now].sum_suf=O[now<<1].sum_suf+O[now<<1|1].sum_suf;
O[now].mx_pre=O[now<<1|1].mx_pre;
O[now].mn_pre=O[now<<1].mn_pre;
O[now].mx_suf=O[now<<1].mx_suf;
O[now].mn_suf=O[now<<1|1].mn_suf;
}
inline void apply_pre(CI now,CI mv)
{
O[now].sum_pre=O[now].len*mv;
O[now].tag_pre=O[now].mx_pre=O[now].mn_pre=mv;
}
inline void apply_suf(CI now,CI mv)
{
O[now].sum_suf=O[now].len*mv;
O[now].tag_suf=O[now].mx_suf=O[now].mn_suf=mv;
}
inline void pushdown(CI now)
{
if (O[now].tag_pre) apply_pre(now<<1,O[now].tag_pre),apply_pre(now<<1|1,O[now].tag_pre),O[now].tag_pre=0;
if (O[now].tag_suf) apply_suf(now<<1,O[now].tag_suf),apply_suf(now<<1|1,O[now].tag_suf),O[now].tag_suf=0;
}
public:
#define TN CI now=1,CI l=1,CI r=n
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void build(TN)
{
O[now]=segment(); O[now].len=r-l+1;
if (l==r)
{
O[now].sum_pre=O[now].mx_pre=O[now].mn_pre=pre[l];
O[now].sum_suf=O[now].mx_suf=O[now].mn_suf=suf[l];
return;
}
int mid=l+r>>1; build(LS); build(RS); pushup(now);
}
inline void assign_pre(CI beg,CI end,CI mv,TN)
{
if (beg>end) return;
if (beg<=l&&r<=end) return apply_pre(now,mv); int mid=l+r>>1; pushdown(now);
if (beg<=mid) assign_pre(beg,end,mv,LS); if (end>mid) assign_pre(beg,end,mv,RS); pushup(now);
}
inline void assign_suf(CI beg,CI end,CI mv,TN)
{
if (beg>end) return;
if (beg<=l&&r<=end) return apply_suf(now,mv); int mid=l+r>>1; pushdown(now);
if (beg<=mid) assign_suf(beg,end,mv,LS); if (end>mid) assign_suf(beg,end,mv,RS); pushup(now);
}
inline int query_pre(CI beg,CI end,TN)
{
if (beg>end) return 0;
if (beg<=l&&r<=end) return O[now].sum_pre; int mid=l+r>>1,ret=0; pushdown(now);
if (beg<=mid) ret+=query_pre(beg,end,LS); if (end>mid) ret+=query_pre(beg,end,RS); return ret;
}
inline int query_suf(CI beg,CI end,TN)
{
if (beg>end) return 0;
if (beg<=l&&r<=end) return O[now].sum_suf; int mid=l+r>>1,ret=0; pushdown(now);
if (beg<=mid) ret+=query_suf(beg,end,LS); if (end>mid) ret+=query_suf(beg,end,RS); return ret;
}
inline int find_pre(CI pos,CI val,TN)
{
if (r<pos||O[now].mx_pre<val) return -1; if (l==r) return l; int mid=l+r>>1; pushdown(now);
int res=find_pre(pos,val,LS); if (res!=-1) return res; return find_pre(pos,val,RS);
}
inline int find_suf(CI pos,CI val,TN)
{
if (l>pos||O[now].mx_suf<val) return -1; if (l==r) return l; int mid=l+r>>1; pushdown(now);
int res=find_suf(pos,val,RS); if (res!=-1) return res; return find_suf(pos,val,LS);
}
inline int find_pos(TN)
{
if (O[now].mx_pre<O[now].mn_suf) return -1; if (l==r) return l; int mid=l+r>>1; pushdown(now);
int res=find_pos(LS); if (res!=-1) return res; return find_pos(RS);
}
#undef TN
#undef LS
#undef RS
}SEG;
signed main()
{
//freopen("M.in","r",stdin);
for (scanf("%lld",&t);t;--t)
{
RI i; int sum=0; for (scanf("%lld",&n),i=1;i<=n;++i) scanf("%lld",&a[i]),sum+=a[i];
for (pre[0]=0,i=1;i<=n;++i) pre[i]=max(pre[i-1],a[i]);
for (suf[n+1]=0,i=n;i>=1;--i) suf[i]=max(suf[i+1],a[i]);
for (SEG.build(),scanf("%lld",&q),i=1;i<=q;++i)
{
scanf("%lld%lld",&x,&y); sum-=a[x]; sum+=(a[x]+=y);
int R=SEG.find_pre(x,a[x]); SEG.assign_pre(x,R==-1?n:R-1,a[x]);
int L=SEG.find_suf(x,a[x]); SEG.assign_suf(L==-1?1:L+1,x,a[x]);
int pos=SEG.find_pos(); if (pos==-1) pos=n+1;
printf("%lld\n",SEG.query_pre(1,pos-1)+SEG.query_suf(pos,n)-sum);
}
}
return 0;
}
Postscript
唉这场的几何和字符串都是极其不可做题,导致虽然前期比较顺但后场我们队又开始无所事事提前下班了
还得来那种几何和字符串是medium~hard的场才能发挥我们队的最大优势啊