The 1st Universal Cup. Stage 15: Hangzhou

Preface

久违的线下训练,结果因为祁神有急事赶回家了就我和徐神两个人打

开场就感觉有点不对劲怎么没有签到,然后全程对着几个题红温还想了一堆假算法,最后愉悦暴毙

感觉这场题本身质量都挺高的,但最后只写了5个(赛后补了一个),等以后有时间再来补补吧


A. Turn on the Light

徐神开场一个神秘做法直接秒了,我直接呃呃

假设当前答案可能的取值区间为 \([l,r]\),找到其最左和最右的两个四等分点 \(p,q\ (p<q)\),询问 \(p,q\)

特判掉问到的点恰好是答案的情况后,如果两次询问后答案不变,则说明区间缩小为 \([p+1,q-1]\);否则说明区间缩小为 \([l,p-1]\cup [q+1,r]\)

实现时可以直接暴力把当前可能的点存下来,总复杂度 \(O(n\log n)\),询问次数 \(2\log n\le 40\)

#include <bits/stdc++.h>

int query(int x) {
	static int pre = 0;
	std::cout << "? " << x + 1 << std::endl;
	int res; std::cin >> res;
	if(res == pre) return std::cout << "! " << x + 1 << std::endl, exit(0), 0;
	return pre =  res;
}

int main() {
	std::ios::sync_with_stdio(false);
	int n; std::cin >> n;
	int pre = 0;
	std::vector<int> a(n);
	for(int i = 0; i < n; ++i) a[i] = i;
	for(;;) {
		int l = a.size() / 4, r = a.size() * 3 / 4;
		int q = (query(a[l]), query(a[r]));
		std::vector<int> b = std::vector<int> {};
		if(q == pre) {
			for(int i = l + 1; i < r; ++i) b.push_back(a[i]);
		} else {
			for(int i = 0; i < l; ++i) b.push_back(a[i]);
			for(int i = r + 1; i < a.size(); ++i) b.push_back(a[i]);
		}
		a = b;
		// for(int i = 0; i < a.size(); ++i) std::cerr << a[i] << char(i == a.size() - 1 ? 10 : 32);
		pre = q;
	}
	return -1;
	
}

C. Puzzle: Kusabi

显然根据题意一个子树不能匹配得留到之后操作的最多只有一个点,现在就讨论贪心地把子树匹配完即可

当“同”的个数为偶数时,此时所有“同”之间必须按深度两两相配,这时候可以处理“长”或者“短”个数差值 \(\le 1\) 的情况

若“短”的个数和“长”的个数相同,那显然就是排序后一一比较;否则若“短”的个数少 \(1\),则贪心地给每个“短”找一个后继匹配是最优的;“短”的个数多 \(1\) 亦同理

当“同”的个数为奇数时,则只能上传多出的那个数

实现的时候注意细节,总复杂度 \(O(n\log n)\)

#include <bits/stdc++.h>

enum {
	NONE = 0,
	TONG,
	CHANG,
	DUAN,
};

void quit() {
	std::cout << "NO\n";
	exit(0);
}

struct label {
	int type, depth, id;	
};

int n;
std::vector<std::pair<int, int>> ans;
std::vector<std::vector<int>> ch;
std::vector<int> nt;

label dfs(int cur, int depth = 1) {
	// std::cerr << "cur = " << cur << char(10);
	std::vector<label> chang, duan, tong;
	auto push_back = [&](label l) {
		switch(l.type) {
			case CHANG: chang.push_back(l); break;
			case  DUAN:  duan.push_back(l); break;
			case  TONG:  tong.push_back(l); break;
		}
	};
	push_back({ nt[cur], depth, cur });
	for(auto ch: ch[cur]) push_back(dfs(ch, depth + 1));
	
	// std::cerr << "cc cur = " << cur << char(10);
	// std::cerr << "tong.size() = " << tong.size() << ", duan.size() == " << duan.size() << ", chang.size() == " << chang.size() << char(10);
	
	if(duan.size() > chang.size() + 1 || chang.size() > duan.size() + 1) quit();
	if(tong.size() % 2 == 1 && duan.size() != chang.size()) quit();
	
	std::sort( tong.begin(), tong.end(), [](label a, label b) { return a.depth < b.depth; });
	std::sort( duan.begin(), duan.end(), [](label a, label b) { return a.depth < b.depth; });
	std::sort(chang.begin(),chang.end(), [](label a, label b) { return a.depth < b.depth; });
	if(tong.size() % 2 == 0) {
		// std::cerr << "debug1 cur = " << cur << " \n";
		for(int i = 0; i < tong.size(); i += 2) {
			if(tong[i].depth != tong[i + 1].depth) quit();
			ans.emplace_back(tong[i].id, tong[i + 1].id);
		}
		if(duan.size() == chang.size()) {
			for(int i = 0; i < duan.size(); ++i) {
				if(duan[i].depth >= chang[i].depth) quit();
				ans.emplace_back(duan[i].id, chang[i].id);
			}
			return { NONE, 0, 0 };	
		} else
		if(duan.size() < chang.size()) {
			label res = {NONE, 0, 0};
			for(int i = 0, j = 0; i < duan.size(); ++i) {
				while(duan[i].depth >= chang[j].depth) {
					if(res.type == NONE) res = chang[j++];
					else quit();
				}
				ans.emplace_back(duan[i].id, chang[j++].id);
			}
			if(res.type == NONE) res = chang.back();
			return res;
		} else {
			label res = {NONE, 0, 0};
			for(int i = chang.size() - 1, j = duan.size() - 1; ~i; --i) {
				while(duan[j].depth >= chang[i].depth) {
					if(res.type == NONE) res = duan[j--];
					else quit();
				}
				ans.emplace_back(duan[j--].id, chang[i].id);
			}
			if(res.type == NONE) res = duan.front();
			return res;
		}
	} else {
		// std::cerr << "debug2 cur = " << cur << " \n";
		for(int i = 0; i < duan.size(); ++i) {
			if(duan[i].depth >= chang[i].depth) quit();
			ans.emplace_back(duan[i].id, chang[i].id);
		}
		label res = { NONE, 0, 0 };
		for(int i = 0; i < tong.size(); ) {
			if(i == tong.size() - 1 || tong[i].depth != tong[i + 1].depth) {
				if(res.type != NONE) quit();
				res = tong[i++];
			} else {
				ans.emplace_back(tong[i].id, tong[i + 1].id);
				i += 2;
			}
		}
		return res;
	}
}

int main() {
	std::ios::sync_with_stdio(false);
	std::cin >> n;
	ch.resize(n + 1), nt.resize(n + 1);
	for(int i = 2, _, p; i <= n; ++i) {
		std::string s;
		std::cin >> _ >> p >> s;
		if(s == "Tong")  nt[i] = TONG;  else
		if(s == "Chang") nt[i] = CHANG; else
		if(s == "Duan")  nt[i] = DUAN;  else
		                 nt[i] = NONE;
		ch[p].push_back(i);
	}
	if(dfs(1).type != NONE) quit();
	std::cout << "YES\n";
	for(auto [a, b]: ans) std::cout << a << ' ' << b << char(10);
	return 0;
}

D. Master of Both III

刚开始的思路是把状态抽象成一个点,然后跑最短路到全为 \(0\) 的状态,但复杂度 \(O(3^n\times n)\) 显然爆炸了

后面徐神发现可以从全为 \(0\) 的状态出发,然后做一个高维后缀 \(\min\) 就可以优化成 \(O(2^n\times n)\)

因为高维后缀 \(\min\) 不需要容斥,因此代码十分好写

#include <bits/stdc++.h>

using llsi = long long signed int;

int n;

inline llsi shift(llsi s, int y) {
	s |= s << y | s >> (n - y);
	s &= (1 << n) - 1;
	return s;
}

llsi a[22];
llsi dp[1 << 22];

inline void chkmn(llsi &a, const llsi &b) {
	if(b < a) a = b;
}

int main () {
	std::ios::sync_with_stdio(false);
	std::cin >> n;
	for(int i = 0; i < n; ++i) std::cin >> a[i];
	memset(dp, 0x3f, sizeof(dp));
	dp[1] = 0;
	for(int i = 1; i < (1 << n); ++i) {
		llsi sta = i;
		for(int j = 1; j < n; ++j) {
			chkmn(dp[shift(sta, n - j)], dp[sta] + a[j]);
		}
		// std::cout << "dp[" << i << "] = " << dp[i] << char(10);
	}
	
	for(int i = (1 << n) - 1; ~i; --i) {
		llsi sta = i;
		for(int j = 0; j < n; ++j) if(!(sta >> j & 1)) {
			chkmn(dp[sta], dp[sta | 1 << j]);
		}
	}
	
	for(int i = 1; i < (1 << n); ++i) {
		// std::cout << "dp[" << i << "] = " << dp[i] << char(10);
	}
	
	llsi ans = 0;
	constexpr llsi mod = 998244353;
	
	for(int i = 1; i < (1 << n); ++i) ans = (ans + dp[i] * i) % mod;
	std::cout << ans << char(10);
	return 0;
}

E. Puzzle: Tapa

很有意思的思维题,首先不难发现最外围一圈要特别处理,要选的空白格就只能在两个相邻的 clue 中间

而中间的情况就比较特殊,虽然允许一个空白格处理对角线相邻的四个 clue,但不难发现这种情况一定可以拆成在两对相邻的 clue 之间放上空白格

因此这题就很简单了,把所有可以匹配的 clue 之间连边,根据网格图的性质这一定是个二分图,看看是否有完美匹配即可

用匈牙利算法实现,复杂度 \(O((nm)^3)\),实际上因为边数很少所以远远跑不满

#include<cstdio>
#include<iostream>
#include<vector>
#include<array>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=105,dx[4]={0,2,0,-2},dy[4]={2,0,-2,0};
int n,m,vis[N*N],match[N*N],x[N*N],y[N*N]; char s[N][N];
vector <int> A,B; vector <array <int,3>> v[N*N];
inline int ID(CI x,CI y)
{
	return (x-1)*(2*m-1)+y;
}
inline void addedge(int a,int b,int c,int d,CI e,CI f)
{
	if (!(s[a][b]=='2'||s[a][b]=='4'||s[a][b]=='7')) return;
	if (!(s[c][d]=='2'||s[c][d]=='4'||s[c][d]=='7')) return;
	if (((a+b)/2)%2) swap(a,c),swap(b,d);
	v[ID(a,b)].push_back({ID(c,d),e,f});
}
inline bool find(CI now,CI idx)
{
	for (auto [to,ex,ey]:v[now])
	if (vis[to]!=idx)
	{
		vis[to]=idx;
		if (!match[to]||find(match[to],idx))
		return match[to]=now,x[to]=ex,y[to]=ey,1;
	}
	return 0;
}
int main()
{
	RI i,j,k; scanf("%d%d",&n,&m);
	for (i=1;i<=2*n-1;++i) scanf("%s",s[i]+1);
	for (i=1;i<=2*n-1;i+=2) for (j=1;j<=2*m-1;j+=2)
	{
		if (!(s[i][j]=='2'||s[i][j]=='4'||s[i][j]=='7')) continue;
		if (((i+j)/2)%2) B.push_back(ID(i,j)); else A.push_back(ID(i,j));
	}
	for (i=1;i+2<=2*n-1;i+=2) addedge(i,1,i+2,1,i+1,1),addedge(i,2*m-1,i+2,2*m-1,i+1,2*m-1);
	for (j=1;j+2<=2*m-1;j+=2) addedge(1,j,1,j+2,1,j+1),addedge(2*n-1,j,2*n-1,j+2,2*n-1,j+1);
	for (i=3;i<=2*n-3;i+=2) for (j=3;j<=2*m-3;j+=2)
	{
		if (!(s[i][j]=='2'||s[i][j]=='4'||s[i][j]=='7')) continue;
		for (k=0;k<4;++k)
		{
			int ii=i+dx[k],jj=j+dy[k];
			if (ii<3||ii>2*n-3||jj<3||jj>2*m-3) continue;
			if (!(s[ii][jj]=='2'||s[ii][jj]=='4'||s[ii][jj]=='7')) continue;
			addedge(i,j,ii,jj,(i+ii)/2,(j+jj)/2);
		}
	}
	//sort(A.begin(),A.end()); A.erase(unique(A.begin(),A.end()),A.end());
	//sort(B.begin(),B.end()); B.erase(unique(B.begin(),B.end()),B.end());
	if (A.size()!=B.size()) return puts("NO"),0;
	int ret=0,cnt=0; for (auto v:A) ret+=find(v,++cnt);
	if (ret!=A.size()) return puts("NO"),0;
	for (auto v:B) s[x[v]][y[v]]='#';
	for (puts("YES"),i=1;i<=2*n-1;++i,putchar('\n'))
	for (j=1;j<=2*m-1;++j)
	{
		char ch=s[i][j];
		if (ch=='.') ch='#'; else if (ch=='#') ch='.';
		putchar(ch);
	}
	return 0;
}

F. Classic: Classical Problem

比赛一上来想了个 bitset 的神秘做法,然后写完交了一发才发现复杂度假了

后面徐神跟我说了一个fix的思路(后面发现沿着这个思路就能过了),但被我扔了跑去想原根的做法了,结果后面又出了个假算法死的透透的

赛后看官方 Tutorial 发现正解是原根转化+二分答案+FFT优化循环卷积的 \(O(p\log^2p)\) 的构式玩意,难写地一批

后面看其他人过的代码发现了一种很好写的根号分治做法,看了下感觉就是赛时思路的优化

考虑若 \(p=n\),此时所有数构成完全剩余系,则答案显然为 \(p\),否则当 \(n\) 变小是就会出现某些数不出现

根据不出现的数个数 \(p-n\) 进行根号分治:

  • \(p-n\le \sqrt p\),则枚举每个 \(c\),再暴力枚举所有没出现的数并更新答案;
  • \(p-n> \sqrt p\),则枚举每个 \(c\),并枚举 \(\operatorname{mex}\) 的值并暴力向后找到第一个不出现的位置,因为此时空洞数量 \(>\sqrt p\),因此平均往后移动的次数是 \(O(\sqrt p)\) 级别的;

综上,总复杂度 \(O(p\sqrt p)\),还非常好写

#include<cstdio>
#include<iostream>
#include<vector>
#include<cmath>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int t,n,p,x,a[N],ans[N];
inline int quick_pow(int x,int mod,int p,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
int main()
{
	//freopen("F.in","r",stdin);
	for (scanf("%d",&t);t;--t)
	{
		RI i,c; for (scanf("%d%d",&n,&p),i=0;i<p;++i) a[i]=0;
		for (i=1;i<=n;++i) scanf("%d",&x),a[x]=1;
		if (!a[0]) { puts("1 1\n0"); continue; } else ans[0]=1;
		if (p-n<=(int)sqrt(p))
		{
			vector <int> vec;
			for (i=0;i<p;++i) if (!a[i]) vec.push_back(i);
			for (c=1;c<p;++c)
			{
				int mex=p;
				for (auto x:vec) mex=min(mex,(int)(1LL*c*x%p));
				ans[c]=mex;
			}
		} else
		{
			for (c=1;c<p;++c)
			{
				int mex=1,inv=quick_pow(c,p,p-2);
				while (a[1LL*mex*inv%p]) ++mex;
				ans[c]=mex;
			}
		}
		int mx=0; vector <int> num;
		for (i=0;i<p;++i) mx=max(mx,ans[i]);
		for (i=0;i<p;++i) if (ans[i]==mx) num.push_back(i);
		printf("%d %d\n",num.size(),mx);
		for (auto x:num) printf("%d ",x); putchar('\n');
	}
	return 0;
}

H. Classic: N Real DNA Pots

开场看了一眼就会了,感觉还是很自然的

考虑二分答案 \(s\),检验的话我们先讲所有点按照横坐标排序

考虑对于 \(i<j\),合法的条件为 \(\frac{y_j-y_i}{x_j-x_i}\ge s\),即 \(y_j-s\times x_j\ge y_i-s\times x_i\)

因此按照 \(y_i-s\times x_i\) 的值做一个最长上升子序列即可,总复杂度 \(O(n\log n\log \frac{1}{\epsilon})\)

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<utility>
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
typedef long double LDB;
typedef pair <int,int> pi;
const int N=100005;
const LDB EPS=1e-9;
int n,m,k,a[N]; pi p[N]; LDB val[N],rst[N];
class Tree_Array
{
	private:
		int bit[N];
	public:
		#define lowbit(x) (x&-x)
		inline void init(void)
		{
			for (RI i=0;i<=m;++i) bit[i]=0;
		}
		inline int get(RI x,int ret=0)
		{
			for (;x;x-=lowbit(x)) ret=max(ret,bit[x]); return ret;
		}
		inline void add(RI x,CI y)
		{
			for (;x<=m;x+=lowbit(x)) bit[x]=max(bit[x],y);
		}
		#undef lowbit
}BIT;
inline int check(const LDB& slp)
{
	RI i; for (i=1;i<=n;++i)
	val[i]=rst[i]=1.0L*p[i].se-slp*p[i].fi;
	sort(rst+1,rst+n+1);
	m=unique(rst+1,rst+n+1)-rst-1; int ret=0;
	for (BIT.init(),i=1;i<=n;++i)
	{
		int tmp=lower_bound(rst+1,rst+m+1,val[i])-rst;
		int cur=BIT.get(tmp)+1; ret=max(ret,cur); BIT.add(tmp,cur);
	}
	return ret;
}
int main()
{
	RI i; for (scanf("%d%d",&n,&k),i=1;i<=n;++i)
	scanf("%d%d",&p[i].fi,&p[i].se); sort(p+1,p+n+1);
	LDB l=-1e9,r=1e9,mid;
	while (r-l>EPS)
	{
		mid=(l+r)/2.0;
		if (check(mid)>=k) l=mid; else r=mid;
	}
	return printf("%.8Lf",l),0;
}

Postscript

感觉可做题还有B和I都没写,等到时候有时间再补吧

posted @ 2024-07-10 20:29  空気力学の詩  阅读(16)  评论(0编辑  收藏  举报