2023-2024 ICPC German Collegiate Programming Contest (GCPC 2023)
Preface
好久没队里一起训练了就周末约了队友去机房VP了一场,同时终于学会了撬机房门这一核心技术
这场总体打的还行,但主要是B全队集体想复杂导致最后没调出来,J徐神写的维护啥的都没问题就是算答案的时候没想清楚,本来可以出11题的
A. Adolescent Architecture 2
徐神总是能在开局就找到每场最难的题目然后开始思考,这题防AK只能说做不了一点
B. Balloon Darts
VP的时候想复杂了导致没写出来这个理论上的Easy-Mid题,实在是可惜
我们队比赛时的做法是先找一个凸包,如果凸包上的点数大于\(6\)则一定无解
否则可以发现对于凸包上的点之间构成的直线中,如果有某条直线经过了四个及以上的点,则这条直线必选
这样一波操作下来可以使剩下要暴力判断的点变成\(\le 9\),就可以直接上暴搜了
但不知道是思路有问题还是实现不行,最后调了半天也没过,看了题解才发现原来是个Easy题
考虑对于某个\(n\)个点,要用\(k\)条直线经过它们的局面,当\(n\le k\)时该局面显然可达
否则考虑找一条直线来缩小问题规模,乍一想我们需要\(O(n^2)\)暴力枚举点对,复杂度就很爆炸
其实仔细思考我们发现由于抽屉原理,我们只需要在前\(k+1\)个点中枚举点对即可
然后每次确定一条直线后就\(O(n)\)扫一遍然后删去所有在直线上的点并递归子问题即可,总复杂度为\(O(\prod_{i=1}^3 C_{i+1}^2 \times n)=O(18\times n)\)
#include<cstdio>
#include<iostream>
#include<vector>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
struct Point
{
int x,y;
inline Point(CI X=0,CI Y=0)
{
x=X; y=Y;
}
friend inline Point operator + (const Point& A,const Point& B)
{
return Point(A.x+B.x,A.y+B.y);
}
friend inline Point operator - (const Point& A,const Point& B)
{
return Point(A.x-B.x,A.y-B.y);
}
};
int n,x,y; vector <Point> p;
inline int Cross(const Point& A,const Point& B)
{
return A.x*B.y-A.y*B.x;
}
inline bool on_line(const Point& A,const Point& B,const Point& C)
{
return Cross(B-A,C-A)==0;
}
inline bool solve(vector <Point> p,CI k)
{
if (p.size()<=k) return 1;
for (RI i=0;i<=k;++i) for (RI j=i+1;j<=k;++j)
{
vector <Point> tmp;
for (auto it:p) if (!on_line(p[i],p[j],it)) tmp.push_back(it);
if (solve(tmp,k-1)) return 1;
}
return 0;
}
signed main()
{
//freopen("B.in","r",stdin); freopen("B.out","w",stdout);
RI i; for (scanf("%lld",&n),i=1;i<=n;++i)
scanf("%lld%lld",&x,&y),p.push_back(Point(x,y));
return puts(solve(p,3)?"possible":"impossible"),0;
}
C. Cosmic Commute
这丁真题过的人怎么不多呢
考虑先BFS求出起点到所有点的最短路,以及所有点到终点的最短路,此时可以先行计算出不经过虫洞的最小代价
接下来考虑枚举在哪个虫洞进行时穿,计算贡献就非常简单了
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=200005,INF=1e9;
int n,m,k,x,y,s[N],t[N],trs[N]; vector <int> v[N];
inline void BFS(CI st,int* dis)
{
queue <int> q; for (RI i=1;i<=n;++i) dis[i]=INF;
q.push(st); dis[st]=0; while (!q.empty())
{
int now=q.front(); q.pop();
for (auto to:v[now]) if (dis[to]>dis[now]+1)
dis[to]=dis[now]+1,q.push(to);
}
}
signed main()
{
//freopen("C.in","r",stdin); freopen("C.out","w",stdout);
RI i; for (scanf("%lld%lld%lld",&n,&m,&k),i=1;i<=k;++i) scanf("%lld",&x),trs[x]=1;
for (i=1;i<=m;++i) scanf("%lld%lld",&x,&y),v[x].push_back(y),v[y].push_back(x);
int sum=0; for (BFS(1,s),BFS(n,t),i=1;i<=n;++i) if (trs[i]) sum+=t[i];
int ans=s[n]*(k-1); for (i=1;i<=n;++i) if (trs[i]) ans=min(ans,s[i]*(k-1)+sum-t[i]);
int g=__gcd(ans,k-1); return printf("%lld/%lld",ans/g,(k-1)/g),0;
}
D. DnD Dice
这题徐神写的,我题目都没看,好像就是个大力背包题,只不过需要用long double
存保证精度
#include <bits/stdc++.h>
using ld = long double;
constexpr int dice[5] = {4, 6, 8, 12, 20};
int c[5], p[502];
long double dp_base[2][501];
int main() {
// freopen("1.in", "r", stdin);
std::ios::sync_with_stdio(false);
for(int &c: c) std::cin >> c;
auto dp1 = dp_base[0], dp2 = dp_base[1];
dp1[0] = 1;
for(int i = 0; i < 5; ++i) {
while(c[i]--) {
memset(dp2, 0, sizeof dp_base[1]);
for(int j = 0; j <= 500 - dice[i]; ++j) for(int k = 1; k <= dice[i]; ++k)
dp2[j + k] += dp1[j];
std::swap(dp1, dp2);
}
}
auto dp = dp1;
for(int i = 1; i <= 500; ++i) p[i] = i;
std::sort(p + 1, p + 500 + 1, [&dp](const int &a, const int &b) {
return dp[a] > dp[b];
});
// for(int i = 1; i <= 10; ++i) std::cout << dp[i] << char(i == 10 ? 10 : 32);
// for(int i = 1; dp[p[i]]; ++i) std::cout << p[i] << ' ' << dp[p[i]] << std::endl;
for(int i = 1; dp[p[i]]; ++i) std::cout << p[i] << char(dp[p[i + 1]] ? 32 : 10);
return 0;
}
E. Eszett
签到题,但因为没看清题目中说最多只用三个连续的S
导致写的偏麻烦了
#include<cstdio>
#include<iostream>
#include<vector>
#include<set>
#include<string>
#define RI register int
#define CI const int&
using namespace std;
const int N=55;
string s; vector <int> pos; set <string> ans;
inline void DFS(CI now,string s)
{
if (now>=s.size()-1) return (void)(ans.insert(s));
if (s[now]=='s'&&s[now+1]=='s')
DFS(now+1,s.substr(0,now)+"B"+s.substr(now+2));
DFS(now+1,s);
}
int main()
{
//freopen("E.in","r",stdin); freopen("E.out","w",stdout);
RI i,j,k; for (cin>>s,i=0;i<s.size();++i) if (s[i]=='S') pos.push_back(i);
string ls; for (i=0;i<s.size();++i) ls+=s[i]-'A'+'a';
DFS(0,ls); for (auto it:ans) cout<<it<<endl;
return 0;
}
F. Freestyle Masonry
在WA了好几发后最后15min灵光一闪上去魔改了个新做法就过了,刺激刺激
这题首先徐神有一个思路就是从下往上一层一层考虑,每次贪心地尽量放横着的块,遇到填不了的空位再放竖着的块,这样正确性显然但实现比较麻烦
然后我就想到那为什么不能从左往右贪心地放,每一列尽量先放竖着的块,把空位留到尽量上方的位置,然后放横着的块
这样维护的时候只需要记录下每一列有哪些位置被占用了即可,但如果直接这么写的话会T飞因为可能会出现占用的位置很多的情况
冷静思考一下我们会发现这些位置间的高度差一定都是\(2\)(手玩会发现如果有多个占用位置的话仅有这种情况),因此可以直接用两个变量维护最小值和最大值,转移的时候讨论下即可
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int n,m,h[N],l=-1,r=-1;
int main()
{
//freopen("F.in","r",stdin); freopen("F.out","w",stdout);
RI i,j; for (scanf("%d%d",&n,&m),i=1;i<=n;++i)
if (scanf("%d",&h[i]),h[i]>m) return puts("impossible"),0;
for (i=1;i<n;++i)
{
if (l==-1&&r==-1)
{
if ((m-h[i])%2==1) l=r=m; continue;
}
if (l<=h[i]) return puts("impossible"),0;
if ((l-h[i]-1)%2==1) --l; else ++l;
if ((m-r)%2==1) ++r; else --r;
if (l>r) l=r=-1;
}
if (l==-1&&r==-1)
{
if ((m-h[n])%2==1) return puts("impossible"),0;
return puts("possible"),0;
}
if (l==r)
{
if (l<=h[n]) return puts("impossible"),0;
if ((l-h[i]-1)%2==1) return puts("impossible"),0;
if ((m-r)%2==1) return puts("impossible"),0;
return puts("possible"),0;
}
return puts("impossible"),0;
}
G. German Conference for Public Counting
签到题,直接统计每种数码最多出现的次数即可,最优的情况肯定是若干个数码的连接,注意\(0\)的情况的处理
#include<cstdio>
#include<iostream>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
int n,ans;
signed main()
{
//freopen("G.in","r",stdin); freopen("G.out","w",stdout);
RI d; int x,p; for (scanf("%lld",&n),d=1;d<=9;++d)
{
for (x=d,p=1;x<=n;x=x*10LL+d,++p); ans+=p-1;
}
for (x=10,p=1;x<=n;x*=10LL,++p); ans+=max(1LL,p-1);
return printf("%lld",ans),0;
}
H. Highway Combinatorics
好劲的题啊,当时后期和徐神一起想了半天的这个题愣是不会,后面发现又是大力随机出奇迹
首先发现这题的本质其实就是找一些斐波那契数,使得它们的和小于等于\(200\)且它们的积模\(10^9+7\)等于\(n\)
题解的做法是一个类似于meet in middle
的做法,先在左右半边各找\(10^6\)个集合,使得每个集合内斐波那契数的下标和小于等于\(100\),然后最后合并两部分即可
考虑当所有数都大致均匀分布的情况下,一共可以得到\(10^{12}\)种数,有很大的概率可以覆盖\(10^9+7\)内的每一个数
但这种做法需要特判\(n=0\)的情形,解决方法就是搞一组无解的情况即可
#include<cstdio>
#include<iostream>
#include<unordered_map>
#include<string>
#include<queue>
#define RI register int
#define CI const int&
using namespace std;
const int mod=1e9+7,LIM=1e6;
int n,fib[105],ifib[105]; unordered_map <int,int> F,G;
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;
}
inline void solve(int* f,unordered_map <int,int>& mp,CI st)
{
queue <int> q; unordered_map <int,int> dis;
q.push(st); mp[st]=0; dis[st]=0;
while (!q.empty()&&mp.size()<LIM)
{
int now=q.front(),step=dis[now]; q.pop();
for (RI i=2;step+i+1<=100;++i)
{
int to=1LL*now*f[i]%mod;
if (mp.count(to)) continue;
dis[to]=step+i+1; mp[to]=i; q.push(to);
}
}
}
int main()
{
//freopen("H.in","r",stdin); freopen("H.out","w",stdout);
if (scanf("%d",&n),n==0) return puts("##.\n.##"),0;
RI i; for (fib[0]=fib[1]=1,i=2;i<=100;++i) fib[i]=(fib[i-1]+fib[i-2])%mod;
for (i=0;i<=100;++i) ifib[i]=quick_pow(fib[i]);
solve(fib,F,1); solve(ifib,G,n);
for (auto [val,step]:F) if (G.count(val))
{
string s=""; int cur=val; while (cur!=1)
{
int lst=F[cur]; s+=string(lst,'.')+"#";
cur=1LL*cur*ifib[lst]%mod;
}
cur=val; while (cur!=n)
{
int lst=G[cur]; s+=string(lst,'.')+"#";
cur=1LL*cur*fib[lst]%mod;
}
cout<<s<<endl<<s; break;
}
return 0;
}
I. Investigating Frog Behaviour on Lily Pad Patterns
很裸的线段树上二分题,只不过题目中输入形式很有迷惑性,需要仔细读题并观察样例才能get到正确的题意
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1200005;
int n,q,x,pos[N];
class Segment_Tree
{
private:
int mi[N<<2];
inline void pushup(CI now)
{
mi[now]=min(mi[now<<1],mi[now<<1|1]);
}
public:
#define TN CI now=1,CI l=1,CI r=1200000
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void updata(CI pos,CI mv,TN)
{
if (l==r) return (void)(mi[now]=mv); int mid=l+r>>1;
if (pos<=mid) updata(pos,mv,LS); else updata(pos,mv,RS); pushup(now);
}
inline int query(CI pos,TN)
{
if (r<pos) return -1; if (mi[now]==1) return -1;
if (l==r) return l; int mid=l+r>>1,res=query(pos,LS);
if (~res) return res; return query(pos,RS);
}
#undef TN
#undef LS
#undef RS
}SEG;
int main()
{
//freopen("I.in","r",stdin); freopen("I.out","w",stdout);
RI i; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&pos[i]),SEG.updata(pos[i],1);
for (scanf("%d",&q),i=1;i<=q;++i)
{
scanf("%d",&x); int y=SEG.query(pos[x]+1);
printf("%d\n",y); SEG.updata(pos[x],0); SEG.updata(pos[x]=y,1);
}
return 0;
}
J. Japanese Lottery
首先我们考虑当已知当前局面代表的排列后怎么计算最后的答案,一个很naive的想法就是求逆序对数,但徐神写了之后发现连样例都过不去
赛后看了题解才发现要从置换环的角度考虑,发现添加/删除一根横杆总是可以使这个排列的置换环个数+1/-1
因此我们最后要得到有\(w\)个置换环的排列\(1,2,\cdots w\)的话就需要\(w\)减去当前排列的置换环个数
至于怎么维护排列可以直接用线段树以高度为下标,每个点的修改相当于一个映射操作,同时pushup
的时候注意下顺序即可
总复杂度\(O(q\log h\times w)\),以下代码是我赛后在徐神比赛时候写的代码上微调得到,因此会有码风的不统一
#include <bits/stdc++.h>
int w, h, q;
constexpr int $h = 2e5 + 5;
struct ars: public std::vector<int> {
ars(): vector() {}
template<typename T>
ars(std::initializer_list<T> list): vector(list) {}
inline ars friend operator *(const ars &a, const ars &b) {
ars c; c.init();
for(int i = 0; i < w; ++i) c[i] = a[b[i]];
return std::move(c);
}
void init() {
resize(w);
for(int i = 0; i < w; ++i) (*this)[i] = i;
}
int getInv() {
static int c[21];
memset(c, 0, sizeof(int) * w);
int res = w;
for(int i = 0; i < w; ++i) {
if (c[i]) continue; --res;
int x=i; while (!c[x]) c[x]=1,x=(*this)[x];
}
return res;
}
};
namespace smt {
constexpr int $node = $h << 2;
ars val[$node];
void init(int p, int l, int r) {
val[p].init();
// std::cout << p << ' ' << val[p].size() << std::endl;
if(l == r) return ;
int lc = p << 1, rc = lc | 1;
int mid = (l + r) >> 1;
init(lc, l, mid);
init(rc, mid + 1, r);
return ;
}
void modify(int p, int pos, int l, int r, const ars& v) {
if(l == r) val[p] = val[p] * v;
else {
int lc = p << 1, rc = lc | 1;
int mid = (l + r) >> 1;
if(pos > mid) modify(rc, pos, mid + 1, r, v);
else modify(lc, pos, l, mid, v);
val[p] = val[lc] * val[rc];
}
}
int getInv() {
return val[1].getInv();
}
}
int main() noexcept {
//freopen("1.in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin >> w >> h >> q;
smt::init(1, 1, h);
while(q--) {
int y, x1, x2;
std::cin >> y >> x1 >> x2;
if (x1>x2) std::swap(x1,x2);
x1--; x2--;
ars v;
v.init();
std::swap(v[x1], v[x2]);
smt::modify(1, y, 1, h, v);
//for(int i = 0; i < w; ++i) std::cout << smt::val[1][i] << char(i == w - 1 ? 10 : 32);
std::cout << smt::getInv() << '\n';
}
return 0;
}
K. Kaldorian Knights
很经典的容斥题,想清楚的话就不难的说
考虑设\(k_i\)的前缀和为\(pfx_i\),不妨定义\(F(n,h)\)表示对于\(n\)个人,需要满足前\(h\)个限制关系的方案数,计算过程可以用以下容斥得到:
这个式子的含义其实手玩一下很好理解,然后乍一看复杂度是很爆炸的,其实不然
除了初始的状态外,所有的状态都满足第一维是原序列的前缀和,同时其对应的下标和第二维的差值总是\(1\)
因此实际上的状态数是\(O(h)\)的,而计算过程需要枚举,因此总复杂度为\(O(h^2)\)
#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=5005,M=1e6+5,mod=1e9+7;
int n,h,pfx[N],f[N][N],fact[M];
inline void dec(int& x,CI y)
{
if ((x-=y)<0) x+=mod;
}
inline int DP(int x,CI y,CI sp=0)
{
if (!y) return fact[sp?x:pfx[x]]; if (!sp&&~f[x][y]) return f[x][y];
int ret=fact[sp?x:pfx[x]]; for (RI i=1;i<=y;++i)
dec(ret,1LL*DP(i,i-1)*fact[(sp?x:pfx[x])-pfx[i]]%mod);
if (sp) return ret; return f[x][y]=ret;
}
int main()
{
//freopen("K.in","r",stdin); freopen("K.out","w",stdout);
RI i; for (scanf("%d%d",&n,&h),i=1;i<=h;++i) scanf("%d",&pfx[i]);
for (i=1;i<=h;++i) pfx[i]+=pfx[i-1];
for (fact[0]=i=1;i<=1000000;++i) fact[i]=1LL*fact[i-1]*i%mod;
return memset(f,-1,sizeof(f)),printf("%d",DP(n,h,1)),0;
}
L. Loop Invariant
经典题,根据常见结论我们知道断开的位置一定是某个前缀为合法的括号序列的位置,然后判断是否和原来相同可以直接大力Hash
#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5;
const int mod1=998244353,mod2=19260817;
struct Hasher
{
int x,y;
inline Hasher(CI X=0,CI Y=0)
{
x=X; y=Y;
}
friend inline bool operator == (const Hasher& A,const Hasher& B)
{
return A.x==B.x&&A.y==B.y;
}
friend inline Hasher operator + (const Hasher& A,const Hasher& B)
{
return Hasher((A.x+B.x)%mod1,(A.y+B.y)%mod2);
}
friend inline Hasher operator - (const Hasher& A,const Hasher& B)
{
return Hasher((A.x-B.x+mod1)%mod1,(A.y-B.y+mod2)%mod2);
}
friend inline Hasher operator * (const Hasher& A,const Hasher& B)
{
return Hasher(1LL*A.x*B.x%mod1,1LL*A.y*B.y%mod2);
}
}h[N],pw[N]; char s[N]; int n,pfx[N];
const Hasher seed=Hasher(31,131);
inline Hasher get(CI l,CI r)
{
return h[r]-h[l-1]*pw[r-l+1];
}
int main()
{
//freopen("L.in","r",stdin); freopen("L.out","w",stdout);
RI i,j; for (scanf("%s",s+1),n=strlen(s+1),pw[0]=Hasher(1,1),i=1;i<=n;++i)
pw[i]=pw[i-1]*seed,h[i]=h[i-1]*seed+Hasher(s[i],s[i]),pfx[i]=pfx[i-1]+(s[i]=='('?1:-1);
for (i=2;i<n;i+=2) if (pfx[i]==0&&!(get(1,n)==(get(i+1,n)*pw[i]+get(1,i))))
{
for (j=i+1;j<=n;++j) putchar(s[j]);
for (j=1;j<=i;++j) putchar(s[j]); return 0;
}
return puts("no"),0;
}
M. Mischievous Math
首先发现若\(d>9\)则直接输出1 2 3
即可,否则我们只需要找一组数使得其无法构造\(\le 9\)的数即可,这里选择了 17 41 91
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int d;
int main()
{
//freopen("M.in","r",stdin); freopen("M.out","w",stdout);
if (scanf("%d",&d),d>9) puts("1 2 3"); else puts("17 41 91");
return 0;
}
Postscript
下周应该还会找机会组织场VP,然后接下来的两个礼拜都有ICPC的网络赛,希望能好好发挥的说