2023-2024 ICPC, Asia Yokohama Regional Contest 2023

Preface

这周末由于要准备CCPC Final的各种事项(当然是作为主办学校的志愿者而不是选手),只有周日有空训练

结果难得的训练感觉我啥也没干就结束了,从头混到尾,一个byd网络流模型感觉和之前重庆市赛那个几乎一样,然后又不会做

后面补题也不积极,有空就在下棋,但你说的对云顶新版本实在是太香了,写完这篇博客接着去玩


A. Yokohama Phenomena

签到,直接用状态\((x,y,d)\)表示处于\((x,y)\)位置,且匹配了前\(d\)个字符的方案数,直接BFS转移即可

#include<cstdio>
#include<iostream>
#include<cstring>
#include<queue>
#include<array>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=15,dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
const char tar[8]={'Y','O','K','O','H','A','M','A'};
int n,m,f[N][N][10],ans; char s[N][N];
signed main()
{
	RI i,j; for (scanf("%lld%lld",&n,&m),i=1;i<=n;++i) scanf("%s",s[i]+1);
	queue <array <int,3>> q;
	for (i=1;i<=n;++i) for (j=1;j<=m;++j)
	if (s[i][j]=='Y') q.push({i,j,0}),f[i][j][0]=1;
	while (!q.empty())
	{
		auto [x,y,d]=q.front(); q.pop();
		if (d==7) { ans+=f[x][y][d]; continue; }
		for (i=0;i<4;++i)
		{
			int nx=x+dx[i],ny=y+dy[i];
			if (nx<1||nx>n||ny<1||ny>m) continue;
			if (s[nx][ny]!=tar[d+1]) continue;
			if (!f[nx][ny][d+1]) q.push({nx,ny,d+1});
			f[nx][ny][d+1]+=f[x][y][d];
		}
	}
	return printf("%lld",ans),0;
}

B. Rank Promotion

诈骗题,但是把我腐乳了,好在祁神没有想我一样下棋把脑子下坏了不然就被签到薄纱了

考虑如果把题目变成:有多少个右端点\(r\)满足,存在至少一个左端点\(l\)使得\(r-l+1\ge c\)且区间\([l,r]\)的正确率符合要求

这是一个比较trivial的问题,我们可以把Y的权值看作\(q-p\)N的权值看作\(-p\),此时正确率的限制等价于区间和\(\ge 0\)

考虑设计一个DP状态,\(f_i\)表示以\(i\)为右端点,向左扩展的长度\(\ge c\)的区间\([l,r]\)中,区间和最大是多少

转移的话也显然,要么\(f_{i+1}\leftarrow f_i+val(i+1)\),要么\(f_{i+1}\leftarrow \sum_{j=i+2-c}^{i+1} val(j)\),有点类似与最大子段和,就是分继承和不继承两种情况

但现在的问题是题目要求的不是合法的右端点数而是等级,我们注意到最后每个位置的等级一定是一段一段的,而如果想要等价发生变化则必须从一个与其等级相同的地方转移来

因此我们可以在每次等价发生变化的点删掉往前的所有状态重新DP,总复杂度\(O(n)\)

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

const int N = 5e5+5;

int n, c, p, q;
string str;
int sum[N], f[N], ans;

signed main(){
	ios::sync_with_stdio(0); cin.tie(0);
	cin >> n >> c >> p >> q;
	cin >> str; str = '$'+str;
	int an = -p, ay = q-p;
	for (int i=1; i<=n; ++i){
		sum[i] = sum[i-1];
		if ('Y'==str[i]) sum[i]+=ay;
		else sum[i]+=an;
	}
	
	int lst=0;
	ans = 0;
	for (int i=1; i<=n; ++i){
		while (i<=n && i-lst<c) ++i;
		if (i>n) break;
		int ai = ('Y'==str[i] ? ay : an);
		f[i] = sum[i-1]-sum[i-c]+ai;
		if (i-lst>c) f[i] = max(f[i], f[i-1]+ai);
		if (f[i]>=0) lst=i, ++ans;
	}
	cout << ans << '\n';
	return 0;	
}

C. Ferris Wheel

好可怕的过题人数,一眼不可做题


D. Nested Repetition Compression

徐神一眼秒的DP,ORZ

首先看数据范围不难想到区间DP,设\(f_{l,r}\)表示完全压缩区间\([l,r]\)的最少步数

除了一些常规的区间DP转移外,剩下需要做的就是考虑将整个区间划分成若干个pattern,可以直接大力枚举重复次数\(2\sim 9\)然后暴力判断

总复杂度\(O(n^3\times A)\),其中\(A\)\(8\)

#include <bits/stdc++.h>

constexpr int $n = 301;

int n; std::string s;

struct Source {
	int type, val;
};

int dp[$n][$n];
Source src[$n][$n];

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

void genAns(int b, int l) {
	if(l == 1) return void(std::cout << s[b]);
	int i = src[b][l].val;
	if(src[b][l].type == 1) {
		genAns(b, i);
		genAns(b + i, l - i);
	} else
	if(src[b][l].type == 2) {
		std::cout << i << "(";
		genAns(b, l / i);
		std::cout << ")";
	} else std::cerr << "src[" << b << "][" << l << "] Not Found\n";
}

int main() {
	std::cin >> s;
	n = s.size();
	for(int i = 0; i < n; ++i) dp[i][1] = 1;
	for(int l = 2; l <= n; ++l) for(int b = 0; b + l <= n; ++b) {
		dp[b][l] = n + 1;
		for(int i = 1; i < l; ++i) if(dp[b][i] + dp[b + i][l - i] < dp[b][l]) {
			dp[b][l] = dp[b][i] + dp[b + i][l - i];
			src[b][l] = Source { 1, i };
		}
		for(int i = 2, j; i <= 9; ++i) if(l % i == 0) {
			for(j = l / i; j < l && s[b + j] == s[b + j - (l / i)]; ++j);
			if(j == l && 3 + dp[b][l / i] < dp[b][l]) {
				dp[b][l] = 3 + dp[b][l / i];
				src[b][l] = Source { 2, i };
			}
		}
	}
	genAns(0, n); std::cout << std::endl;
	return 0;
}


E. Chayas

ORZ祁神几乎单切了这个诡异的Counting,虽然因为常数等问题又卡时间又卡内存,但好在是淦过去了

首先考虑把所有\(b\)相同的限制拿出来一起看,不难发现此时的要求就是\(a,c\)要在\(b\)异侧,不妨考虑转化成黑白染色问题在两点间连条边

首先如果得到的图存在奇环则显然无解,否则考虑状压DP

\(f_{mask}\)表示确定了状态为\(mask\)的点的方案数,对于某个固定的\(b\),转移的话就是要找到所有合法的染色状态\(S\),然后从\(S\)可以转移到\(S|(1<<b)\)

发现最后状态的转移得到的一定是个DAG,然后就可以统计方案数了

注意实现的时候需要注意常数,比如找合法染色状态的时候要用DFS遍历所有Case而不是先枚举最后的情况再遍历处理,同时为了防止MLE以及优化常数,存状态间的边时可以用bitset来存

总复杂度\(O(2^n\times n)\)

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

const int N = (1<<24)+5;
const int B = 30;
const int MOD = 998244353;
void add(int &x, int a){if ((x+=a)>=MOD) x-=MOD;}
void mul(int &x, int a){x=1LL*x*a%MOD;}

int n, m;
//vector<int> G[N];
bitset <25> G[N];
vector<int> rG[B][B];
int col[B], id[B], val[B][2], idx;
vector<int> pc[B];
int dp[N];

bool dfs(int rt, int x){
	id[x] = idx;
	for (int v : rG[rt][x]){
		if (id[v]>0){
			if (col[v] == (col[x]^1)) continue;
			else return false;
		}
		col[v] = col[x]^1;
		if (!dfs(rt, v)) return false;
	}
	return true;
}

void dfs2(int x, int cur, int S){
	if (cur==idx){
//		printf("add(%d %d)\n", S, (S|(1LL<<(x-1))));
		//G[S].push_back((S|(1LL<<(x-1))));
		G[S][x-1]=1;
	}else{
		dfs2(x, cur+1, S|val[cur+1][0]);
		dfs2(x, cur+1, S|val[cur+1][1]);
	}
}

bool solve(int x){
	memset(col, 0, sizeof(col));
	memset(id, -1, sizeof(id));
	memset(val, 0, sizeof(val));
	idx=0;
	for (int i=1; i<=n; ++i){
		if (x == i) continue;
		if (id[i]>0) continue; 
		++idx;
		if (!dfs(x, i)) return false;
	}
//	printf("solve(%d) idx=%d\n", x, idx);
//	printf("id:"); for (int i=1; i<=n; ++i) printf("%d ", id[i]); puts("");
//	printf("col:"); for (int i=1; i<=n; ++i) printf("%d ", col[i]); puts("");
	for (int i=1; i<=n; ++i){
		if (i==x) continue;
		if (col[i]) val[id[i]][0]|=(1<<(i-1));
		else val[id[i]][1]|=(1<<(i-1));
	}
	dfs2(x, 0, 0);
//	for (int i=0; i<(1LL<<idx); ++i){
//		int S=0;
//		for (int j=1; j<=n; ++j){
//			if (j==x) continue;
//			int c = col[j];
//			if (i&(1<<(id[j]-1))) c^=1;
//			if (c) S|=(1<<(j-1));
//		}
////		G[S].push_back((S|(1LL<<(x-1))));
//		G[S][x-1]=true;
//	}
	return true; 
}

signed main(){
	//freopen("E.in","r",stdin);
	ios::sync_with_stdio(0); cin.tie(0);
	cin >> n >> m;
	for (int i=1; i<=m; ++i){
		int a, b, c; cin >> a >> b >> c;
		rG[b][a].push_back(c);
		rG[b][c].push_back(a);	
	}
	bool ok=true;
	for (int i=1; i<=n; ++i){
		if (!solve(i)){ok=false; break;}
	}
	if (!ok) cout << "0\n";
	else{
		for (int i=0; i<(1LL<<n); ++i){
			pc[__builtin_popcount(i)].push_back(i);	
		}
		dp[0] = 1;
		for (int i=0; i<n; ++i){
			for (int S : pc[i]){
				for (int j=0; j<n; ++j) if (G[S][j]){
					int v = (S|(1LL<<j));
					add(dp[v], dp[S]);	
				}
			}
		}
//		for (int i=0; i<(1LL<<n); ++i) printf("dp[%d]=%d\n", i, dp[i]);
		cout << dp[(1LL<<n)-1] << '\n';
	}
	return 0;	
}

F. Color Inversion on a Huge Chessboard

注意到每个同色块一定是矩形后就很简单了

显然行和列的贡献独立,单独对行考虑,把翻转了一次的状态看作\(1\),否则看作\(0\)

则行的贡献就是一个数组中不同颜色的段数,这个很好\(O(1)\)维护修改

对列的情况同理,最后贡献相乘得到最后的答案

#include<cstdio>
#include<iostream>
#include<string>
#define RI register int
#define CI const int&
using namespace std;
const int N=500005;
int n,m,r[N],c[N],x,R,C; string s;
int main()
{
	ios::sync_with_stdio(false); cin.tie(0);
	RI i; for (cin>>n>>m,i=1;i<=n;++i) r[i]=c[i]=i&1;
	for (R=C=n,i=1;i<=m;++i)
	{
		cin>>s>>x;
		if (n==1) { puts("1"); continue; }
		if (s[0]=='R')
		{
			r[x]^=1;
			if (x==1)
			{
				if (r[x+1]==r[x]) --R; else ++R;
			} else if (x==n)
			{
				if (r[x-1]==r[x]) --R; else ++R;
			} else
			{
				if (r[x-1]==r[x]&&r[x+1]==r[x]) R-=2;
				else if (r[x-1]!=r[x]&&r[x+1]!=r[x]) R+=2;
			}
		} else
		{
			c[x]^=1;
			if (x==1)
			{
				if (c[x+1]==c[x]) --C; else ++C;
			} else if (x==n)
			{
				if (c[x-1]==c[x]) --C; else ++C;
			} else
			{
				if (c[x-1]==c[x]&&c[x+1]==c[x]) C-=2;
				else if (c[x-1]!=c[x]&&c[x+1]!=c[x]) C+=2;
			}
		}
		cout<<1LL*R*C<<'\n';
	}
	return 0;
}

G. Fortune Telling

徐神最后1h写这个题然后被记忆化的边界搞红温了,然而我和祁神已经跑去花雕占座了,最后有点可惜赛后10min改出来

这题主要刚开始走了很多弯路,从对称性之类的下手,搞了半天感觉也不太对劲

后面索性考虑不如直接大力DP,设\(f_{n,k}\)表示剩下\(n\)个人时,第\(k\)个人活下来的概率

然后要注意到\(f_{n,\cdot}\)只和\(f_{\frac{5n}{6},\cdot},f_{\frac{5n}{6}+1,\cdot}\)有关(这里的除法默认下取整,下同),同理后面的两个只与\(f_{\frac{25n}{36},\cdot},f_{\frac{25n}{36}+1,\cdot},f_{\frac{25n}{36}+2,\cdot}\)有关

因此要计算整个过程设计到的状态总数,其实就是要求\(n\times \sum_{i=0}^\infty (i+1)\times (\frac{5}{6})^i\)的值

后面那个东西经典的等差乘等比,手玩一下会发现收敛到\(36\),因此直接大力DP的状态数也就\(36n\)级别

那么直接枚举每次投出的数转移即可,总复杂度\(O(6^3\times n)\)

实现的时候为了减小常数可以暴力算出前面的DP值,然后再记忆化求解

#include <bits/stdc++.h>

using llsi = long long signed int;

constexpr llsi mod = 998244353;

constexpr llsi ksm(llsi a, llsi b) {
	llsi res = 1;
	while(b) {
		if(b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}

constexpr inline llsi inv(llsi a) { return ksm(a, mod - 2); }

constexpr llsi inv6 = inv(6);

constexpr int $t = 10000;

int dp_base[$t * ($t + 1) >> 1];

inline int& dp(int i, int j) {
	return dp_base[(i * (i - 1) >> 1) + j - 1];
}

void add(int &a, int b) { if((a += b) >= mod) a -= mod; }

std::vector<std::vector<int>> mem;

int get_dp(int i, int J) {
	if(i <= $t) return dp(i, J);
	if(J > i / 2) J = i - J + 1;
	if(!mem[i].size()) {
		mem[i].assign((i + 3) / 2, 0);
		for(int j = 1, r = i; j <= r; j++, r--)
			for(int k = 1; k <= 6; ++k) if((j - k + 6) % 6)
				add(mem[i][j], inv6 * get_dp(i - (i - k + 6) / 6, j - (j - k + 6) / 6) % mod);
	}
	return mem[i][J];
}

int main() {
	// std::cerr << "sizeof(dp_base) = " << sizeof(dp_base) / 1024.L / 1024.L << char(10);
	int n; std::cin >> n;

	mem.resize(n + 1);

	for(int i = 1; i <= 6; ++i) for(int j = 1; j <= i; ++j) dp(i, j) = inv(i);
	for(int i = 7; i <= $t; ++i) for(int j = 1; j <= i; ++j) {
		for(int k = 1; k <= 6; ++k) if((j - k + 6) % 6)
			add(dp(i, j), inv6 * dp(i - (i - k + 6) / 6, j - (j - k + 6) / 6) % mod);
	}

	std::vector<int> ans(n + 1);

	for(int l = 1, r = n; l <= r; ++l, --r) ans[l] = ans[r] = get_dp(n, l);
	int tot = 0;
	for(int i = 1; i <= n; ++i) std::cout << ans[i] << char(10), add(tot, ans[i]);

	// std::cerr << "tot = " << tot << char(10);

	return 0;
}

H. Task Assignment to Two Employees

首先这题如果只有一个人的话要找到最优的顺序就是个经典贪心问题

对于两个工作\(i,j\),当仅考虑它们两个时,把\(i\)放在\(j\)之前产生的贡献就是\(s_i\times v_j\);反之把\(i\)放在\(j\)之后产生的贡献就是\(s_j\times v_i\)

因此如果两个工作\(i,j\)最后被确定分给了同一人(假设是第一个人),则它会带来的贡献就是\(\max(s_{1,i}\times v_{1,j},s_{1,j}\times v_{1,i})\)

现在的问题就是要给每个任务确定具体分配给哪个人,这个问题可以用最小割求解:

  • 将每个任务看作一个点\(i\),同时设立源点\(S\)和汇点\(T\)
  • 对于不同的两点\(i,j\),连接\(i\leftrightarrow j\)的双向边,其中每条边的容量都是\(\max(s_{1,i}\times v_{1,j},s_{1,j}\times v_{1,i})+\max(s_{2,i}\times v_{2,j},s_{2,j}\times v_{2,i})\)
  • 每个点\(i\)设一个顶标\(h_{1/2,i}\)记录其按照上述规则练出的和第\(1/2\)人相关的边权之和
  • 最后\(S\to i\)\(2\times p_0\times v_{1,i}+h_{1,i}\)的边,\(i\to T\)\(2\times p_0\times v_{2,i}+h_{2,i}\)的边

不难发现跑最小割后最后保留下来的边就是合法的选择策略,注意到之前计算贡献翻倍了因此需要除以\(2\)

#include<cstdio>
#include<iostream>
#include<queue>
#include<cstring>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=105,INF=1e18;
int n,p0,S[2][N],V[2][N],h[2][N],all;
namespace Network_Flow
{
	const int NN=105,MM=1e6+5;
	struct edge
	{
		int to,nxt,v;
	}e[MM<<1]; int cnt=1,head[NN],cur[NN],dep[NN],s,t; queue <int> q;
    inline void addedge(CI x,CI y,CI z)
    {
        e[++cnt]=(edge){y,head[x],z}; head[x]=cnt;
        e[++cnt]=(edge){x,head[y],0}; head[y]=cnt;
    }
    #define to e[i].to
    inline bool BFS(void)
    {
        memset(dep,0,(t+1)*sizeof(int));
		dep[s]=1; q=queue <int>(); q.push(s);
        while (!q.empty())
        {
            int now=q.front(); q.pop();
			for (RI i=head[now];i;i=e[i].nxt)
            if (e[i].v&&!dep[to]) dep[to]=dep[now]+1,q.push(to);
        }
        return dep[t];
    }
    inline int DFS(CI now,CI tar,int dis)
    {
        if (now==tar) return dis; int ret=0;
        for (RI& i=cur[now];i&&dis;i=e[i].nxt)
        if (e[i].v&&dep[to]==dep[now]+1)
        {
            int dist=DFS(to,tar,min(dis,e[i].v));
            if (!dist) dep[to]=INF;
            dis-=dist; ret+=dist; e[i].v-=dist; e[i^1].v+=dist;
            if (!dis) return ret;
        }
        if (!ret) dep[now]=0; return ret;
    }
    #undef to
    inline int Dinic(int ret=0)
    {
        while (BFS()) memcpy(cur,head,(t+1)*sizeof(int)),ret+=DFS(s,t,INF); return ret;
    }
};
using namespace Network_Flow;
signed main()
{
	RI i,j; scanf("%d%d",&n,&p0); s=n+1; t=s+1;
	for (j=0;j<2;++j) for (i=1;i<=n;++i) scanf("%d",&S[j][i]);
	for (j=0;j<2;++j) for (i=1;i<=n;++i) scanf("%d",&V[j][i]);
	for (i=1;i<=n;++i) for (j=1;j<=n;++j) if (i!=j)
	{
		int x0=max(S[0][i]*V[0][j],S[0][j]*V[0][i]);
		int x1=max(S[1][i]*V[1][j],S[1][j]*V[1][i]);
		all+=x0+x1; addedge(i,j,x0+x1); h[0][i]+=x0; h[1][j]+=x1;
	}
	for (i=1;i<=n;++i)
	{
		int x0=2LL*p0*V[0][i],x1=2LL*p0*V[1][i]; all+=x0+x1;
		addedge(s,i,h[0][i]+x0); addedge(i,t,h[1][i]+x1);
	}
	return printf("%lld",(all-Dinic())/2LL),0;
}

I. Liquid Distribution

题目都没看,弃疗


J. Do It Yourself?

同上,直接跑路


K. Probing the Disk

好像是个比较签的交互几何题,我们队开场就写了但我不知道题意

#include<bits/stdc++.h>
using namespace std;
using LD = long double;
const LD eps = 1e-8;

int sgn(LD x){return fabs(x)<=eps ? 0 : x>eps ? 1 : -1;}
LD sqr(LD x){return x*x;}

void askx(int x){
	cout << "query " << x << ' ' << 0 << ' ' << x << ' ' << (int)1e5 << endl;
}
void asky(int x){
	cout << "query " << 0 << ' ' << x << ' ' << (int)1e5 << ' ' << x << endl;
}
signed main(){
	LD res=0.0L;
	int px=0, py=0;
	int ansx=0, ansy=0, ansR=0;
	
	do{
		px += 199;
		if (px > (int)1e5) break;
		askx(px);
		cin >> res;
	}while (res < eps);
	LD l1 = res*0.5L, l2=0.0L;
	askx(px-1);
	cin >> l2;
	l2*=0.5L;
	int x=0;
	if (sgn(sqr(l2)-sqr(l1)+1) >= 0){
		x = round(0.5L*(sqr(l2)-sqr(l1)+1));
		ansx = px-x;
	}else{
		x = round(0.5L*(sqr(l1)-sqr(l2)-1));
		ansx = px+x;
	}
	ansR = round(sqrtl(sqr(x)+sqr(l1)));
	
	do{
		py += 199;
		if (py > (int)1e5) break;
		asky(py);
		cin >> res;
	}while (res < eps);
	l1 = res*0.5L, l2=0.0L;
	asky(py-1);
	cin >> l2;
	l2*=0.5L;
	if (sgn(sqr(l2)-sqr(l1)+1) >= 0){
		int y = round(0.5L*(sqr(l2)-sqr(l1)+1));
		ansy = py-y;
	}else{
		int y = round(0.5L*(sqr(l1)-sqr(l2)-1));
		ansy = py+y;
	}
		
	cout << "answer " << ansx << ' ' << ansy << ' ' << ansR << endl;
	return 0;	
}

Postscript

唉这就是霓虹场吗跟Atcoder一个逼样全是Counting,真是让闪总做不了一点

posted @ 2024-03-26 15:55  空気力学の詩  阅读(134)  评论(0编辑  收藏  举报