The 2022 ICPC Polish Collegiate Programming Contest (AMPPZ 2022)

空気力学の詩發表於2024-07-17

Preface

今天由於是我們隊搬毒瘤場,因此下午就不用集中訓練索性繼續 VP UCup

這場題很有外國場的風格,程式碼量和科技含量都不大,只要動動腦筋就行了,最後也是剛好打到了 10 題下班


A. Aliases

不難發現假設 \(a=b=0\),則 \(c\le \log_{10} n\le 7\),因此只要考慮 \(a+b+c\le 7\) 的情況,直接列舉即可

#include<cstdio>
#include<iostream>
#include<string>
#include<map>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int t,n,pw[10]; string A[N],B[N];
int main()
{
	ios::sync_with_stdio(0); cin.tie(0);
	RI i; for (pw[0]=i=1;i<=7;++i) pw[i]=pw[i-1]*10;
	for (cin>>t;t;--t)
	{
		for (cin>>n,i=1;i<=n;++i) cin>>A[i]>>B[i];
		for (RI sum=1;sum<=7;++sum)
		{
			for (RI a=0;a<=sum;++a)
			for (RI b=0;a+b<=sum;++b)
			{
				int c=sum-a-b,mx=0;	map <string,int> rst;
				for (i=1;i<=n;++i)
				{
					string tmp=A[i].substr(0,a)+B[i].substr(0,b);
					mx=max(mx,++rst[tmp]);
					if (mx>pw[c]) break;
				}
				if (mx<=pw[c])
				{
					cout<<a<<' '<<b<<' '<<c<<'\n';
					goto exit;
				}
			}
		}
		exit:;
	}
	return 0;
}

B. Bars

這題在去年的 2023年電子科技大學ACM-ICPC暑假前集訓-數學 裡做過,因此直接貼當時寫的題解了

首先考慮樸素的DP做法,不難想到設\(f_i\)表示\(i\)位置為關鍵點,\([1,i]\)這些位置能得到的最大價值

轉移的時候就是列舉上一個關鍵點\(j\),此時\([j,i-1]\)中這些位置的右邊第一個關鍵點都是\(a_i\),且\([j+1,i]\)這些位置的左邊第一個關鍵點都是\(a_j\)

因此不難寫出轉移方程:

\[f_i=\max_\limits{j<i} f_j+(i-j)\times (a_i+a_j) \]

這種DP方程第一眼考慮決策單調性或者斜率最佳化,但實操一下會發現沒法處理,因為既有\(j\times a_i\)的項,也有\(i\times a_j\)的項

看似走投無路了,但根據不那麼顯然的觀察可以看出,\((i-j)\times (a_i+a_j)\)是以\((i,0).(i,a_i),(j,0),(j,a_j)\)四點構成的梯形面積的兩倍

因此我們可以把原題意轉化為,給定平面上的若干點\((i,a_i)\),求它們與\(x\)軸形成的最大面積的多邊形

那麼很顯然就是求這些點構成的凸包即可,直接單調棧搞一波,複雜度\(O(n)\)

#include<bits/stdc++.h>
using namespace std;
#define int long long

const int N = 5e5+5;
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};}
}pt[N];
int cross(Pt p, Pt a, Pt b){return (a-p).crs(b-p);}

int t, n, A[N];
Pt stk[N]; int tp=0;

signed main(){
	ios::sync_with_stdio(0); cin.tie(0);
	cin >> t;
	while (t--){
		cin >> n;
		for (int i=1; i<=n; ++i) cin >> A[i], pt[i]=Pt{i, A[i]};
		tp=-1;
		stk[++tp] = pt[1];
		stk[++tp] = pt[2];
		for (int i=3; i<=n; ++i){
			while (tp>0 && cross(stk[tp-1], stk[tp], pt[i])>=0) --tp;
			stk[++tp] = pt[i];
		}
//		for (int i=0; i<=tp; ++i) printf("(%lld %lld)\n", stk[i].x, stk[i].y);
		int ans=0;
		for (int i=1; i<=tp; ++i){
			ans += (stk[i-1].y+stk[i].y)*(stk[i].x-stk[i-1].x);	
		}
		cout << ans << '\n';
	}
	return 0;	
}

C. Ctrl+C Ctrl+V

簽到,令 \(f_{i,mask}\) 表示處理了前 \(i\) 位,從第 \(i\) 位往前的 \(mask\) 狀態是否被修改,轉移十分顯然

#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,INF=1e9;
int t,f[N][1<<4]; char s[N];
int main()
{
	for (scanf("%d",&t);t;--t)
	{
		RI i,j,k; scanf("%s",s+1); int n=strlen(s+1);
		if (n<=3) { puts("0"); continue; }
		for (i=1;i<=n;++i) for (j=0;j<16;++j) f[i][j]=INF;
		for (j=0;j<16;++j) f[4][j]=__builtin_popcount(j);
		if (s[1]=='a'&&s[2]=='n'&&s[3]=='i'&&s[4]=='a') f[4][0]=INF;
		for (i=4;i<n;++i) for (j=0;j<16;++j) if (f[i][j]!=INF)
		{
			for (k=0;k<2;++k)
			{
				int mask=((j<<1)&15)|k;
				if (mask==0&&s[i-2]=='a'&&s[i-1]=='n'&&s[i]=='i'&&s[i+1]=='a') continue;
				f[i+1][mask]=min(f[i+1][mask],f[i][j]+k);
			}
		}
		int ans=INF; for (j=0;j<16;++j) ans=min(ans,f[n][j]);
		printf("%d\n",ans);
	}
	return 0;
}

D. Dazzling Mountain

簽到,對於每個點 \(x\),求出其子樹大小 \(sz_x\),然後用一個桶判斷下每種 \(sz\) 對應的葉子數量是否等於總數即可

#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5;
int t,n,x,y,leaf,l[N],sz[N],bkt[N]; vector <int> v[N];
inline void DFS(CI now=1,CI fa=0)
{
	l[now]=0; sz[now]=1;
	for (auto to:v[now]) if (to!=fa)
	DFS(to,now),l[now]+=l[to],sz[now]+=sz[to];
	if (sz[now]==1) ++l[now],++leaf;
	bkt[sz[now]]+=l[now];
}
int main()
{
	for (scanf("%d",&t);t;--t)
	{
		RI i; for (scanf("%d",&n),i=1;i<=n;++i) v[i].clear(),bkt[i]=0;
		for (i=1;i<n;++i) scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
		vector <int> ans; leaf=0;
		for (DFS(),i=1;i<=n;++i) if (bkt[i]==leaf) ans.push_back(i);
		//for (i=1;i<=n;++i) printf("%d %d\n",sz[now],l[now]);
		printf("%d\n",ans.size());
		for (auto x:ans) printf("%d ",x); putchar('\n');
	}
	return 0;
}

E. Euclidean Algorithm

想到關鍵就很簡單的一個題

首先要注意到 \(2a-b\) 的變化可以看作在數軸上有若干個點,每次可以把某個點 \(b\) 作關於另一個點 \(a\) 的對稱點

不難發現無論怎麼操作任意兩點之間的距離都不會縮小,最小值就是初始時的 \(b-a\)

因此有解的充要條件就是 \(\gcd(a,b)=a\bmod (b-a)\),統計合法的方案數也非常套路

不妨先欽定 \(\gcd(a,b)=1\) 對應的答案為 \(f(x)\),則最後答案為 \(\sum_{i=1}^n f(\lfloor\frac{n}{i}\rfloor)\)

滿足 \(1=a\bmod (b-a)\) 的對數也很好計算,列舉 \(b-a\) 的值 \(j\),則畫畫圖會發現只有 \(\lfloor\frac{x-1}{j}\rfloor\) 種方案,則 \(f(x)=\sum_{j=1}^{x-1} \lfloor\frac{x-1}{j}\rfloor\)

用除法分塊套除法分塊,時間複雜度 \(O(n^{\frac{3}{4}})\),由於時限很大可以透過(跑了 24531ms)

#include<cstdio>
#include<iostream>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
int t,n;
inline void write(const __int128& x)
{
	if (x>=10) write(x/10); putchar(x%10+'0');
}
signed main()
{
	for (scanf("%lld",&t);t;--t)
	{
		scanf("%lld",&n); __int128 ans=0;
		auto f=[&](int n)
		{
			__int128 ret=0; --n;
			for (int l=1,r;l<=n;l=r+1)
			r=n/(n/l),ret+=__int128(r-l+1)*(n/l);
			return ret;
		};
		for (int l=1,r;l<=n;l=r+1)
		r=n/(n/l),ans+=__int128(r-l+1)*f(n/l);
		write(ans); putchar('\n');
	}
	return 0;
}

F. Flower Garden

感覺不太可做,棄療


G. Great Chase

這題祁神看了眼就秒了,我題意都不知道

#include<bits/stdc++.h>
using namespace std;
#define int long long

using LD = long double;
const LD eps = 1e-8;
int sgn(LD x){return fabs(x)<=eps ? 0 : (x>eps ? 1 : -1);}

const int N = 4e5+5;
int t, n, p[N], v[N];
const LD INF = 1e18;

bool check(LD x){
	LD mx=-INF, mn=INF;
	for (int i=1; i<=n; ++i){
		if (p[i]>0) mn=min(mn, p[i]-v[i]*x);
		else mx=max(mx, p[i]+v[i]*x);
	}
	return sgn(mx-mn)<0;
}
signed main(){
	ios::sync_with_stdio(0); cin.tie(0);
	cout << setiosflags(ios::fixed) << setprecision(10);
	cin >> t;
	while (t--){
		cin >> n >> v[0];
		for (int i=1; i<=n; ++i) cin >> p[i] >> v[i];
		LD L=0.0L, R=1e13;
		for (int tt=0; tt<200; ++tt){
//			printf("L=%Lf R=%Lf\n", L, R);
			LD M  = (R+L)*0.5L;
			if (check(M)) L=M;
			else R=M;	
		}
		cout << L*v[0] << '\n';
	}
	return 0;	
}

H. Hyperloop

看起來也是個神仙題,棄療


I. Investors

沒有證明,全是猜測,但就是能過題.jpg

首先直覺告訴我們這個問題等價於把原序列分成 \(k+1\) 段,每段之間不會相互影響,因此總逆序對數量就是所有段逆序對數量之和

大力 DP 複雜度顯然是 \(O(n^3)\) 的,但這種分段類的 DP 一般肯定會有決策單調性,懶得證明了直接爬上去寫完就過了

由於這場沒有題解我也不知道這玩意咋證的,總之能過就行,複雜度 \(O(n^2\log n)\)

#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=6005,INF=1e9;
int t,n,m,k,a[N],rst[N],g[N][N],f[N][N];
class Tree_Array
{
	private:
		int bit[N];
	public:
		#define lowbit(x) (x&-x)
		inline int get(RI x,int ret=0)
		{
			for (;x<=m;x+=lowbit(x)) ret+=bit[x]; return ret;
		}
		inline void add(RI x,CI y)
		{
			for (;x;x-=lowbit(x)) bit[x]+=y;
		}
		#undef lowbit
}BIT;
inline void solve(CI k,CI l=1,CI r=n,CI L=1,CI R=n)
{
	if (l>r) return; int mid=l+r>>1,pos;
	for (RI i=L;i<=R&&i<=mid;++i)
	if (f[k-1][i-1]+g[i][mid]<f[k][mid])
	f[k][mid]=f[k-1][i-1]+g[i][mid],pos=i;
	solve(k,l,mid-1,L,pos); solve(k,mid+1,r,pos,R);
}
int main()
{
	for (scanf("%d",&t);t;--t)
	{
		RI i,j; for (scanf("%d%d",&n,&k),i=1;i<=n;++i)
		scanf("%d",&a[i]),rst[i]=a[i];
		sort(rst+1,rst+n+1); m=unique(rst+1,rst+n+1)-rst-1;
		for (i=1;i<=n;++i) a[i]=lower_bound(rst+1,rst+m+1,a[i])-rst;
		for (i=1;i<=n;++i)
		{
			g[i][i]=0; BIT.add(a[i],1);
			for (j=i+1;j<=n;++j)
			g[i][j]=g[i][j-1]+BIT.get(a[j]+1),BIT.add(a[j],1);
			for (j=i;j<=n;++j) BIT.add(a[j],-1);
		}
		for (++k,i=1;i<=n;++i) f[1][i]=g[1][i];
		for (i=2;i<=k;++i)
		{
			for (j=1;j<=n;++j) f[i][j]=INF;
			solve(i);
		}
		printf("%d\n",f[k][n]);
	}
	return 0;
}

J. Job for a Hobbit

看起來是防 AK 題,棄療


K. Kooky Tic-Tac-Toe

大力模擬題,由於我們隊開場開題順序有問題所以導致罰時極其抽象,因此定下鐵律如果有人開場 1h 內上機寫了 100 行以上的東西就自動下機

具體判斷時可以列舉最後下的一顆棋子,使得拿去這顆子後遊戲不結束,但細節還是比較多的

#include<bits/stdc++.h>
using namespace std;

const int N = 10;
int t, n, k;

struct Node{
	string tbl[N];	
};

using pii = pair<int, int>;

int calc(Node &cur, int r, int c, int dr, int dc){
	int res=1;
	for (int x=1; x<=n; ++x){
		int nr=r+dr*x, nc=c+dc*x;	
		if (nr<=0 || nr>n || nc<=0 || nc>n) break;
		if (cur.tbl[nr][nc]!=cur.tbl[r][c]) break;
		++res;
	}
	return res;
}

pii findChain(Node &cur){
	int anso=0, ansx=0;
	for (int i=1; i<=n; ++i){
		for (int j=1; j<=n; ++j){
			if ('.'==cur.tbl[i][j]) continue;
			if ('o'==cur.tbl[i][j]){
				anso = max(anso, calc(cur, i, j, 0, 1));
				anso = max(anso, calc(cur, i, j, 1, 0));
				anso = max(anso, calc(cur, i, j, 1, 1));
				anso = max(anso, calc(cur, i, j, 1, -1));
			}
			if ('x'== cur.tbl[i][j]){
				ansx = max(ansx, calc(cur, i, j, 0, 1));
				ansx = max(ansx, calc(cur, i, j, 1, 0));
				ansx = max(ansx, calc(cur, i, j, 1, 1));
				ansx = max(ansx, calc(cur, i, j, 1, -1));
			}
		}
	}
	return {anso, ansx};
}

void genMove(vector<pii> &res, Node &cur, char ch){
	vector<pii> vec1, vec2;
	for (int i=1; i<=n; ++i) for (int j=1; j<=n; ++j){
		if ('.'==cur.tbl[i][j]) continue;
		if (ch==cur.tbl[i][j]) vec1.emplace_back(i, j);
		else vec2.emplace_back(i, j);
	}
	
	res.clear();
	for (int i=0; i<(int)vec1.size(); ++i){
		res.push_back(vec1[i]);
		if (i<vec2.size()) res.push_back(vec2[i]);
	}
}

void solve(){
	
}

signed main(){
	ios::sync_with_stdio(0); cin.tie(0);
	cin >> t;
	while (t--){
		Node nd;
		cin >> n >> k;
//		printf("n=%d k=%d\n", n, k);
		for (int i=1; i<=n; ++i){
			cin >> nd.tbl[i];
			nd.tbl[i] = '$' + nd.tbl[i];
		}
		
		int cntx=0, cnto=0, cntd=0;
		for (int i=1; i<=n; ++i) for (int j=1; j<=n; ++j){
			if ('o'==nd.tbl[i][j]) ++cnto;
			else if ('x'==nd.tbl[i][j]) ++cntx;	
			else ++cntd;
		}
		
		if (abs(cntx-cnto)>1){
			cout << "NIE\n";	
			continue;
		}
		
		auto [anso, ansx] = findChain(nd);
		bool ok=true;
		if (ansx>=k && anso>=k) ok=false;
		if (ansx>=2*k || anso>=2*k) ok=false;
		
		if (!ok) cout << "NIE\n";
		else{
			char winner='.';
			if (ansx>=k) winner='x';
			if (anso>=k) winner='o';
			
		
			if ('.'==winner){
				if (0==cntd){
					vector<pii> res;
					genMove(res, nd, (cntx>cnto ? 'x' : 'o'));
					cout << "TAK\n";
					for (auto [x, y] : res){
						cout << x << ' ' << y << '\n';	
					}
				}else cout << "NIE\n";
			}else if (('o'==winner && cnto<cntx) || ('x'==winner && cntx<cnto)){
				cout << "NIE\n";
			}else{
				ok=false;
				int ar, ac;
				Node nxt;
				for (int i=1; i<=n; ++i){
					for (int j=1; j<=n; ++j){
						if (winner==nd.tbl[i][j]){
							nxt=nd;
							nxt.tbl[i][j]='.';
							auto [lo, lx] = findChain(nxt);
							if (lo<k && lx<k){
								ar=i, ac=j;
								ok=true; break;
							}
						}
					}	
					if (ok) break;
				}
				
				
				
				if (!ok) cout << "NIE\n";
				else{
					vector<pii> res;
					char ch;
					if (cntx!=cnto){
						ch = (cntx>cnto ? 'x' : 'o');
					}else{
						ch = (winner=='o' ? 'x' : 'o');
					}
					
					genMove(res, nxt, ch);
					cout << "TAK\n";
					for (auto [x, y] : res){
						cout << x << ' ' << y << '\n';	
					}
					cout << ar << ' ' << ac << '\n';
				}
			}
		}
	}
	return 0;	
}

L. Line Replacements

很有思維含量的一個題

首先考慮倒著處理,把刪邊變成加邊,再次之前先判斷給出的森林是否合法

顯然對於每個聯通塊,葉子節點必須都是加油站,而中間的非加油站節點的判定就需要思考一下

對於某個非加油站節點,設與其相鄰的所有邊的權值和為 \(sum\),顯然 \(sum\) 必須為偶數

同時令這些邊中邊權最大的為 \(mxval\),若 \(mxval>\frac{sum}{2}\) 則一定無解

再考慮集合中的邊,在加入一條兩段不同時為加油站的邊時,首先要滿足加入的邊邊權為偶數,其次要滿足當前加入的邊的邊權不超過此時這個點相鄰的邊權和

不難發現加邊時的限制和判斷原聯通塊合法的限制是一體兩面的,而加邊的策略也很簡單,就是按照邊權從小到大排序即可

#include <bits/stdc++.h>

std::vector<int> work() {
	int n, p;
	std::cin >> n >> p;
	std::vector<bool> hkr(n, false);
	std::vector<int64_t> hbk(n, 0);
	std::vector<int> deg(n, 0);
	std::vector<std::array<int, 3>> edge(n - 1);
	
	for(int i = 0, s; i < p; ++i) std::cin >> s, hkr[s - 1] = true;
	for(int i = 0; i < n - 1; ++i) {
		auto &[f, t, val] = edge[i];
		std::cin >> f >> t >> val;
		deg[--f]++, deg[--t]++;
	}
	
	int k; std::cin >> k;
	std::vector<int> ars(k);
	std::vector<bool> dld(n - 1, false);
	for(auto &ars: ars) std::cin >> ars, dld[--ars] = true;
	
	for(int i = 0; i < n - 1; ++i) if(dld[i]) {
		auto [f, t, _] = edge[i];
		deg[f]--, deg[t]--;
	} else {
		auto [f, t, v] = edge[i];
		hbk[f] += v, hbk[t] += v;
	}
	
	for(int i = 0; i < n; ++i) if(!hkr[i] && (deg[i] == 1 || hbk[i] % 2 == 1)) return {};
	
	// for(int i = 0; i < n; ++i) std::cout << hbk[i] << char(i == n - 1 ? 10 : 32);
	for(int i = 0; i < n - 1; ++i) if(!dld[i]) {
		auto [f, t, v] = edge[i];
		
		if(v + v > hbk[f] && !hkr[f] || v + v > hbk[t] && !hkr[t]) return {};
	}
	// std::cout << "Blyat\n";
	
	std::sort(ars.begin(), ars.end(), [&](const int &x, const int &y) {
		return edge[x][2] < edge[y][2];
	});
	
	for(int i = 0; i < ars.size(); ++i) {
		auto [f, t, v] = edge[ars[i]];
		dld[i] = false;
		if(hkr[f] && hkr[t]) continue;
		if(v % 2) return {};
		
		for(auto c: (int[2]){f, t}) if(!hkr[c]) {
			if(v > hbk[c]) return {};
			hbk[c] += v;
		}
	}
	std::reverse(ars.begin(), ars.end());
	return ars;
}

int main() {
	std::ios::sync_with_stdio(false);
	int z; std::cin >> z; while(z--) {
		auto ans = work();
		if(ans.empty()) std::cout << "NIE\n";
		else {
			std::cout << "TAK\n";
			for(int i = 0; i < ans.size(); ++i) std::cout << ans[i] + 1 << char(i == ans.size() - 1 ? 10 : 32);
		}
	}
	return 0;
}


M. Minor Evil

注意到同樣可以倒著處理,將殺人改為復活,則顯然若從後往前的某天可以復活一個人,則這個人被複活一定是最優的

#include <bits/stdc++.h>

int main() {
	std::ios::sync_with_stdio(false);
	int z; std::cin >> z; while(z--) {
		int n, k; std::cin >> n >> k;
		
		std::vector<bool> alive(n, true);
		std::vector<std::pair<int, int>> edge(k);
		
		for(auto &[f, t]: edge) {
			std::cin >> f >> t;
			f--, t--;
		}
		
		int s; std::cin >> s;
		for(int i = 0, si; i < s; ++i) {
			std::cin >> si;
			alive[--si] = false;
		}
		
		std::string ans(k, 'N');
		for(int i = k - 1; ~i; --i) {
			auto [a, b] = edge[i];
			if(alive[a] && !alive[b]) {
				ans[i] = 'T';
				alive[b] = true;
			}
		}
		
		
		int i;
		for(i = 0; i < n; ++i) if(!alive[i]) {
			std::cout << "NIE\n";
			break;
		}
		
		if(i == n) std::cout << "TAK\n" << ans << char(10);
	}
	return 0;
}

Postscript

這就是神秘外國場啊,打完一掃前兩天每天幾個題的陰霾,瞬間找回自信

相關文章