The 2023 Guangdong Provincial Collegiate Programming Contest
Preface
由于马上就要打校赛了,赶紧找点难度稍微低点的比赛练练手
这场由于选题的时候没注意挑了个有两题是做过的比赛来打,本来以为是轻松拿捏,没想到是红温发病
搞了半天最后终于勉强搞出9个题,感觉时间足够能出个10题的说
A. Programming Contest
纯签到
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=10005;
int t,y_1,y_2,n,x,ext[N];
int main()
{
for (scanf("%d",&t);t;--t)
{
RI i; scanf("%d%d",&y_1,&n);
for (i=1970;i<=9999;++i) ext[i]=1;
for (i=1;i<=n;++i) scanf("%d",&x),ext[x]=0;
int rk=1; scanf("%d",&y_2);
for (i=y_1;i<y_2;++i) rk+=ext[i];
printf("%d\n",rk);
}
return 0;
}
B. Base Station Construction
感觉我发病了,把一个简单题复杂化了,但无伤大雅能过就行
首先不难发现如果一个区间\(A\)包含了另一个区间\(B\),则区间\(A\)可以直接扔掉不管
因此我们把区间按照左端点排序后,那单调栈维护一下右端点,即可得到两个端点都单调的区间集合
此时对于每个位置\(i\),显然它能控制到的区间编号一定连续,不妨记为\([l_i,r_i]\)
那么此时可以考虑DP,设\(f_{i,j}\)表示考虑了前\(i\)个位置,控制了编号在\(1\sim j\)中的所有区间,最小代价是多少
转移的化分两种情况,选择\(a_i\)的情况需要满足\(j+1\ge l_i\),因为中间不能有空缺
手玩一下会发现拿个树状数组维护下第二维下标在\(0\sim m\)的DP数组,转移的话就是个求后缀\(\min\)和单点修改
#include<cstdio>
#include<iostream>
#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;
using namespace std;
const int N=500005,INF=1e18;
int t,n,m,a[N],l[N],r[N],f[N]; pi itv[N];
class Segment_Tree
{
private:
int val[N<<2],tag[N<<2];
inline void apply(CI now,CI mv)
{
val[now]=tag[now]=mv;
}
inline void pushdown(CI now)
{
if (tag[now]) apply(now<<1,tag[now]),apply(now<<1|1,tag[now]),tag[now]=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(CI st,TN)
{
val[now]=st; tag[now]=0; if (l==r) return;
int mid=l+r>>1; build(st,LS); build(st,RS);
}
inline void modify(CI beg,CI end,CI mv,TN)
{
if (beg<=l&&r<=end) return apply(now,mv); int mid=l+r>>1; pushdown(now);
if (beg<=mid) modify(beg,end,mv,LS); if (end>mid) modify(beg,end,mv,RS);
}
inline void solve(int* arr,TN)
{
if (l==r) return (void)(arr[l]=val[now]);
int mid=l+r>>1; pushdown(now); solve(arr,LS); solve(arr,RS);
}
#undef TN
#undef LS
#undef RS
}SEG;
class Tree_Array
{
private:
int bit[N];
public:
#define lowbit(x) (x&-x)
inline void init(int* f)
{
for (RI i=1;i<=m+1;++i) bit[i]=f[i];
}
inline int get(RI x,int ret=INF)
{
for (;x<=m+1;x+=lowbit(x)) ret=min(ret,bit[x]); return ret;
}
inline void add(RI x,CI y)
{
for (;x;x-=lowbit(x)) bit[x]=min(bit[x],y);
}
#undef lowbit
}BIT;
signed main()
{
for (scanf("%lld",&t);t;--t)
{
RI i,j; for (scanf("%lld",&n),i=1;i<=n;++i) scanf("%lld",&a[i]);
for (scanf("%lld",&m),i=1;i<=m;++i) scanf("%lld%lld",&itv[i].fi,&itv[i].se);
sort(itv+1,itv+m+1); static int stk[N]; int top=0;
for (i=1;i<=m;++i)
{
while (top&&itv[stk[top]].se>=itv[i].se) --top;
stk[++top]=i;
}
for (m=top,i=1;i<=m;++i) itv[i]=itv[stk[i]];
//for (i=1;i<=m;++i) printf("[%d,%d]\n",itv[i].fi,itv[i].se);
for (SEG.build(n+1),i=m;i>=1;--i)
SEG.modify(itv[i].fi,itv[i].se,i); SEG.solve(l);
for (SEG.build(0),i=1;i<=m;++i)
SEG.modify(itv[i].fi,itv[i].se,i); SEG.solve(r);
//for (i=1;i<=n;++i) printf("%d : (%d,%d)\n",i,l[i],r[i]);
for (f[1]=0,i=2;i<=m+1;++i) f[i]=INF; BIT.init(f);
for (i=1;i<=n;++i)
{
if (l[i]>r[i]) continue;
int val=BIT.get(l[i])+a[i];
if (val<f[r[i]+1]) BIT.add(r[i]+1,f[r[i]+1]=val);
}
printf("%lld\n",f[m+1]);
}
return 0;
}
C. Trading
不难发现最优策略一定是先将所有店按照\(a_i\)升序排序,然后一段前缀选择买入,一段后缀选择卖出
考虑枚举分界点后,需要买入/卖出的个数就是两边的最小值,不妨分两种情况来讨论,钦定较小的一边在前缀/后缀
那么此时另一边的选法用two pointers
扫一下就可以确定了,总复杂度\(O(n)\)
#include<cstdio>
#include<iostream>
#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=100005;
int t,n; pi p[N];
signed main()
{
for (scanf("%lld",&t);t;--t)
{
RI i,j; for (scanf("%lld",&n),i=1;i<=n;++i)
scanf("%lld%lld",&p[i].fi,&p[i].se);
sort(p+1,p+n+1); int ans=0,ret=0,sum1=0,sum2=0;
for (i=1,j=n;i<=n;++i)
{
sum1+=p[i].se; ret-=p[i].fi*p[i].se;
while (j>=i&&sum2+p[j].se<sum1) sum2+=p[j].se,ret+=p[j].fi*p[j].se,--j;
if (i>j) break;
ans=max(ans,ret+(sum1-sum2)*p[j].fi);
}
ret=sum1=sum2=0;
for (i=n,j=1;i>=1;--i)
{
sum1+=p[i].se; ret+=p[i].fi*p[i].se;
while (j<=i&&sum2+p[j].se<sum1) sum2+=p[j].se,ret-=p[j].fi*p[j].se,++j;
if (i<j) break;
ans=max(ans,ret-(sum1-sum2)*p[j].fi);
}
printf("%lld\n",ans);
}
return 0;
}
D. New Houses
首先不难发现对于\(a_i\ge b_i\)的人,直接先让他们住在一起肯定是最优的
剩下的人如果空位允许它们每个人单独坐那肯定最优,否则就要考虑选出一部分人和前面确定的人坐在一起
手玩一下会发现此时把一个单独的人分出去对答案的减少量为\(b-a\),贪心地每次把最小的分出去即可
#include<bits/stdc++.h>
using namespace std;
#define int long long
int t, n, m;
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cin >> t;
while (t--){
cin >> n >> m;
int ans=0;
int cnt=0;
vector<int> vec;
int mn = (int)1e18+5;
for (int i=1; i<=n; ++i){
int a, b; cin >> a >> b;
if (a>=b){
ans+=a;
++cnt;
}else{
ans+=b;
vec.push_back(b-a);
}
mn = min(max(a, b)-min(a, b), mn);
}
sort(vec.begin(), vec.end());
if (0==cnt && 2*n-1<=m){
cout << ans << '\n';
}else if (1==cnt && 2*n-1<=m){
cout << ans-mn << '\n';
}else{
int pos=0;
bool ok=false;
while (2*n-cnt>m){
ok=true;
ans -= vec[pos++];
++cnt;
// printf("n=%lld m=%lld cnt=%lld\n", n, m, cnt);
}
// printf("cnt=%lld ok=%lld\n", cnt, ok);
cout << ans << '\n';
}
}
return 0;
}
E. New but Nostalgic Problem
徐神一眼秒的string,但后面听了下做法感觉好像确实不难
考虑拿Trie维护所有的串,当我们枚举一条从根出发的路径表示答案时,考虑选择哪些串是合法的
显然所有字典序小于路径的串都是可以选的,并且当前路径对应的子树中也是可以随便选,但对于字典序大于路径的串每个子树内只能取至多一个串
在字典树上一边DFS一边确定各个部分的串的数量即可
#include <bits/stdc++.h>
constexpr int $n = 1000005;
int n, k, go[$n][26], s[$n], h[$n], O;
void dfs1(int cur) {
for(auto c: go[cur]) if(c) dfs1(c), s[cur] += s[c];
}
char ans[$n];
bool dfs2(int cur, int t, int q) {
ans[t] = '\0';
int H = h[cur];
for(auto c: go[cur]) if(c) H += bool(s[c]);
if (H >= 2 && H + q >= k) return true;
q += h[cur];
for(auto c: go[cur]) if(c) q += bool(s[c]);
for(int i = 0; i < 26; ++i) {
int c = go[cur][i]; if(!c) continue;
ans[t] = i + 'a';
q -= bool(s[c]);
if(dfs2(c, t + 1, q)) return true;
q += s[c];
}
return false;
}
void work() {
O = 1; memset(go[O], 0, sizeof(go[O])); s[O] = 0; h[O] = 0;
std::cin >> n >> k;
for(int i = 0; i < n; ++i) {
std::string S; std::cin >> S;
int cur = 1;
// std::cerr << S << char(10);
for(auto c: S) {
// std::cerr << c;
int u = c - 'a';
if(!go[cur][u]) {
go[cur][u] = ++O;
memset(go[O], 0, sizeof(go[O])); s[O] = 0; h[O] = 0;
}
cur = go[cur][u];
}
// std::cerr << "\n";
s[cur] += 1;
h[cur] += 1;
}
dfs1(1);
dfs2(1, 0, 0);
if(!ans[0]) std::cout << "EMPTY\n";
else std::cout << ans << char(10);
return ;
}
int main() {
// freopen("1.in", "r", stdin);
std::ios::sync_with_stdio(false);
int T; std::cin >> T; while(T--) work();
return 0;
}
F. Traveling in Cells
这题此时在去年暑假一轮集训的时候见过,当时写了个二分套线段树的铸币做法喜提TLE
今天终于想出个看似正常的写法,但前面因为unordered_map
常数太大被卡飞了,后面用了新科技gp_hash_table
直接艹过
这次做法和上次的区别其实就是写了线段树上二分,但像这种定下一个端点再二分的题感觉写起来很不好搞
后面YY了一个很神秘的从叶子往上走,找到第一个不合法的位置再往下走的奇妙写法,后面竟然一遍过了
当然由于要把所有信息放到一棵线段树上因此需要给每个节点记录下每种颜色有多少个,这个拿上面讲的gp_hash_table
维护一下即可
最后理论总复杂度是\(O(\sum k\log n)\)的,但我实现的常数略大因此跑的很紧
#include <bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#define int long long
#define RI register int
#define CI const int&
#define Tp template <typename T>
using namespace std;
const int N=100005;
int t,n,q,opt,a[N],c[N],pos[N],x,y,ansl,ansr;
using namespace __gnu_pbds;
const int RANDOM = chrono::high_resolution_clock::now().time_since_epoch().count();
struct chash {
int operator()(int x) const { return x ^ RANDOM; }
};
typedef gp_hash_table<int, int, chash> hash_t;
class FileInputOutput
{
private:
static const int S=1<<21;
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
#define pc(ch) (Ftop!=Fend?*Ftop++=ch:(fwrite(Fout,1,S,stdout),*(Ftop=Fout)++=ch))
char Fin[S],Fout[S],*A,*B,*Ftop,*Fend; int pt[20];
public:
inline FileInputOutput(void) { Ftop=Fout; Fend=Fout+S; }
Tp inline void read(T& x)
{
x=0; char ch; while (!isdigit(ch=tc()));
while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
}
Tp inline void write(T x,const char ch='\n')
{
RI ptop=0; while (pt[++ptop]=x%10,x/=10);
while (ptop) pc(pt[ptop--]+48); pc(ch);
}
inline void flush(void)
{
fwrite(Fout,1,Ftop-Fout,stdout);
}
#undef tc
#undef pc
}F;
class Tree_Array
{
private:
int bit[N];
public:
#define lowbit(x) (x&-x)
inline void init(void)
{
for (RI i=0;i<=n;++i) bit[i]=0;
}
inline int get(RI x,int ret=0)
{
for (;x;x-=lowbit(x)) ret+=bit[x]; return ret;
}
inline void add(RI x,CI y)
{
for (;x<=n;x+=lowbit(x)) bit[x]+=y;
}
#undef lowbit
}BIT;
class Segment_Tree
{
private:
int anc[N<<2],L[N<<2],R[N<<2]; hash_t num[N<<2];
#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 bool check(CI now,vector <int>& vec)
{
int cnt=0; for (auto col:vec) cnt+=num[now][col];
return cnt==R[now]-L[now]+1;
}
inline void left_binary(vector <int>& vec,CI now,CI l,CI r)
{
if (l==r)
{
if (check(now,vec)) ansl=l; return;
}
int mid=l+r>>1;
if (check(now<<1|1,vec)) ansl=L[now<<1|1],left_binary(vec,LS);
else left_binary(vec,RS);
}
inline void right_binary(vector <int>& vec,CI now,CI l,CI r)
{
if (l==r)
{
if (check(now,vec)) ansr=r; return;
}
int mid=l+r>>1;
if (check(now<<1,vec)) ansr=R[now<<1],right_binary(vec,RS);
else right_binary(vec,LS);
}
public:
inline void build(TN)
{
num[now].clear(); L[now]=l; R[now]=r;
if (l==r) return (void)(pos[l]=now); int mid=l+r>>1;
anc[now<<1]=anc[now<<1|1]=now; build(LS); build(RS);
}
inline void updata(CI pos,CI col,CI mv,TN)
{
num[now][col]+=mv; if (l==r) return; int mid=l+r>>1;
if (pos<=mid) updata(pos,col,mv,LS); else updata(pos,col,mv,RS);
}
inline void left_up(vector <int>& vec,CI now,CI l,CI r)
{
if (now==1) return; int fa=anc[now];
if (now==(fa<<1)) return left_up(vec,fa,L[fa],R[fa]);
if (check(fa<<1,vec)) return ansl=L[fa],left_up(vec,fa,L[fa],R[fa]);
return left_binary(vec,fa<<1,L[fa<<1],R[fa<<1]);
}
inline void right_up(vector <int>& vec,CI now,CI l,CI r)
{
if (now==1) return; int fa=anc[now];
if (now==(fa<<1|1)) return right_up(vec,fa,L[fa],R[fa]);
if (check(fa<<1|1,vec)) return ansr=R[fa],right_up(vec,fa,L[fa],R[fa]);
return right_binary(vec,fa<<1|1,L[fa<<1|1],R[fa<<1|1]);
}
#undef TN
#undef LS
#undef RS
}SEG;
signed main()
{
//freopen("F.in","r",stdin); freopen("F.out","w",stdout);
for (F.read(t);t;--t)
{
RI i; F.read(n); F.read(q); BIT.init(); SEG.build();
for (i=1;i<=n;++i) F.read(c[i]),SEG.updata(i,c[i],1);
for (i=1;i<=n;++i) F.read(a[i]),BIT.add(i,a[i]);
while (q--)
{
F.read(opt); F.read(x); F.read(y);
if (opt==1) SEG.updata(x,c[x],-1),SEG.updata(x,c[x]=y,1);
else if (opt==2) BIT.add(x,-a[x]),BIT.add(x,a[x]=y); else
{
vector <int> vec(y); for (i=0;i<y;++i) F.read(vec[i]);
ansl=x; SEG.left_up(vec,pos[x],x,x);
ansr=x; SEG.right_up(vec,pos[x],x,x);
F.write(BIT.get(ansr)-BIT.get(ansl-1));
}
}
}
return F.flush(),0;
}
G. Swapping Operation
看过题人数不是很可做啊
H. Canvas
这题看起来挺可做的,但后面没啥时间了就没认真想,留着以后再说吧
I. Path Planning
不难发现从小到大枚举,强制枚举到的数都要在一条路径上
显然合法的充要条件就是把所有出线的位置的二元组\((x_i,y_i)\)排序后,第二维依然要保持有序
直接拿个set
维护下所有二元组,当某次插入破坏了上述性质就说明不合法
#include<cstdio>
#include<iostream>
#include<utility>
#include<set>
#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=1e6+5;
int t,n,m,x; pi pos[N];
int main()
{
for (scanf("%d",&t);t;--t)
{
RI i,j; set <pi> rst;
for (scanf("%d%d",&n,&m),i=1;i<=n;++i)
for (j=1;j<=m;++j) scanf("%d",&x),pos[x]={i,j};
for (i=0;i<=n*m;++i)
{
if (i==n*m) { printf("%d\n",n*m); break; }
rst.insert(pos[i]);
auto it=rst.find(pos[i]);
if (it!=rst.begin())
{
auto pre=it; --pre;
if (pre->se>it->se)
{
printf("%d\n",i); break;
}
}
auto nxt=it; ++nxt;
if (nxt!=rst.end())
{
if (it->se>nxt->se)
{
printf("%d\n",i); break;
}
}
}
}
return 0;
}
J. X Equals Y
这题徐神开场就看了而且也想出了基本正确的做法,但由于机时问题就没来得及写,坐等徐神补题吧
K. Peg Solitaire
大力暴搜题,直接状压棋盘状态然后枚举所有可能的情况即可
#include<cstdio>
#include<iostream>
#include<unordered_set>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=10,dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
int t,n,m,k,x,y,ans; unordered_set <int> rst;
inline int ID(CI x,CI y)
{
return x*m+y;
}
inline void DFS(CI mask,CI num)
{
ans=min(ans,num); if (num==1) return;
for (RI i=0;i<n;++i) for (RI j=0;j<m;++j)
if ((mask>>ID(i,j))&1LL)
{
for (RI k=0;k<4;++k)
{
int rx=i+dx[k],ry=j+dy[k];
if (rx<0||rx>=n||ry<0||ry>=m) continue;
if (!((mask>>ID(rx,ry))&1LL)) continue;
int tx=rx+dx[k],ty=ry+dy[k];
if (tx<0||tx>=n||ty<0||ty>=m) continue;
if ((mask>>ID(tx,ty))&1LL) continue;
int nmask=mask^(1LL<<ID(i,j))^(1LL<<ID(rx,ry))^(1LL<<ID(tx,ty));
if (rst.count(nmask)) continue;
rst.insert(nmask); DFS(nmask,num-1);
}
}
}
signed main()
{
for (scanf("%lld",&t);t;--t)
{
RI i; scanf("%lld%lld%lld",&n,&m,&k);
int mask=0; for (i=1;i<=k;++i)
scanf("%lld%lld",&x,&y),--x,--y,mask|=(1LL<<ID(x,y));
ans=k; rst.insert(mask); DFS(mask,k);
printf("%lld\n",ans); rst.clear();
}
return 0;
}
L. Classic Problem
不可做题,看了题解上来的前置知识我就不知道,直接寄
M. Computational Geometry
祁神比赛的时候写了半天,又是卡边界又是卡内存的,结果后面还是一直过不去
后面我冷静分析发现原来是INF
设小了没绷住,只能说确实坑
这题好像就是先用DP求一下选择某两个端点后两边内部直径的最大值,然后暴枚两个端点求答案即可,可以预处理两边多边形面积来保证不选到面积为零的Case
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e3+5;
const int INF = (int)4e18+5;
int t, n, f[2*N][N], sum[2*N][N];
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(){return x*x+y*y;}
}pt[2*N];
int cross(Pt &p, Pt &a, Pt &b){return (a-p).crs(b-p);}
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cin >> t;
while (t--){
cin >> n;
for (int i=0; i<n; ++i){
int x, y; cin >> x >> y;
pt[i].x=pt[n+i].x=x;
pt[i].y=pt[n+i].y=y;
}
for (int i=0; i<2*n; ++i){
sum[i][0]=0;
for (int j=1; j<=min(n-1, 2*n-1-i); ++j){
sum[i][j] = sum[i][j-1]+pt[i+j-1].crs(pt[i+j]);
}
}
for (int i=0; i<2*n; ++i){
for (int j=1; j<=min(n-1, 2*n-1-i); ++j){
sum[i][j] += pt[i+j].crs(pt[i]);
}
}
// int tp=1;
// for (int i=2; i<n; ++i){
// if (0==cross(pt[tp-1], pt[tp], pt[i])) pt[tp]=pt[i];
// else pt[++tp]=pt[i];
// }
// while (tp>0 && cross(pt[0], pt[1], pt[tp])==0) pt[0]=pt[tp--];
// n = tp+1;
// for (int i=0; i<n; ++i) pt[n+i]=pt[i];
// for (int i=0; i<n*2; ++i) printf("(%lld %lld)\n", pt[i].x, pt[i].y);
//
for (int i=0; i<2*n; ++i){
f[i][0]=0;
for (int j=1; j<=min(n-1, 2*n-i-1); ++j){
f[i][j] = max(f[i][j-1], (pt[i]-pt[i+j]).len2());
// printf("i=%lld j=%lld g=%lld\n", i, j, g[i][j]);
}
}
f[2*n-1][0] = 0;
for (int i=2*n-2; i>=0; --i){
f[i][0] = 0;
for (int j=1; j<min(n, 2*n-i); ++j){
f[i][j] = max(f[i+1][j-1], f[i][j]);
// printf("i=%lld j=%lld f=%lld\n", i, j, f[i][j]);
}
}
int ans = INF;
for (int i=0; i<n; ++i){
for (int j=2; j<=min(n-2, 2*n-i-1); ++j){
if (sum[i][j]!=0 && sum[i+j][n-j]!=0) ans = min(ans, f[i][j]+f[i+j][n-j]);
// printf("i=%lld j=%lld f[i][j]=%lld f[i+j][n-j]=%lld res=%lld\n", i, j, f[i][j], f[i+j][n-j], f[i][j]+f[i+j][n-j]);
}
}
cout << ans << '\n';
}
return 0;
}
Postscript
感觉现在比赛很容易红温爆炸啊,怎么回事呢