牛客多校2024-6

A - Cake

(神金,害我调了一个半小时)
AliceBob 玩一个游戏。游戏分为2阶段。
阶段1:有一棵边权值为 \(0\)\(1\) 的有根树,两人轮流走,Alice 先走,走到叶子就停下来。记录下经过边的权值形成一个字符串\(S\)
阶段2:Bob 将一个蛋糕切成 \(len(S)\) 块,块可以为空。然后遍历 \(S\) 的每个字符,如果为 \(0\)Alice 选择一块蛋糕;如果为 \(1\)Bob 选择一块蛋糕。
每个人都期望利益最大化,求 Bob 获得的蛋糕比例最大值。
有根树节点数 \(n \leq 2e5\)


首先考虑阶段2:
显然如果 \(S[0] = \ '1'\)Bob 能获得整块蛋糕。将这个性质推广下去,Bob 能够将蛋糕切成任意的 \(k (k \leq len(S))\) 块,从而使 \(S[k]\) 以后的字符串失去了意义。
这意味着 Bob 能够选择 \(S\) 中的一个前缀 \(S_{pre}\) 切蛋糕。然后考虑 \(k\) 块怎么切,容易发现均等切是最优的,因为在 Alice 先手的情况下更大的蛋糕块会被 Alice 拿走从而不利(Bob 先手的情况答案为1)。

然后考虑阶段1:
显然这是一个树上博弈,Alice 需要让任意 \(S_{pre}\)\(1\) 的占比最大值最小,Bob 需要让 \(S_{pre}\)\(1\) 的占比最大值最大。
设计dfs状态为dfs(e, cnt0, cnt1)代表进行到节点e,完成subtree(e)的计算时该玩家(可由cnt0+cnt1的奇偶得出)的最优解,于是就做完了。
此外还要注意 Alice 在最优解仍然可能走向权值为 \(1\) 得边,从而使答案变大,因此在除了根节点的每个节点都应该尝试更新答案的最大值,而不是仅在 Bob 的回合尝试更新。


const int U = 5e5;
int head[U], val[U], to[U], nxt[U], tot;
void add(int x, int y, int z)
{
	to[++tot] = y; val[tot] = z; nxt[tot] = head[x]; head[x] = tot;
}
bool vis[U];
double dfs(int e, int cnt0, int cnt1)
{
	int t = (cnt0+cnt1+1)%2;
	double ans = !t;
	vis[e] = 1;
	
	bool leaf = 1;
	for(int g = head[e]; g; g = nxt[g])
	{
		int y = to[g], z = val[g];
		if (vis[y]) continue;
		leaf = 0;

		if (t == z)
		{
			if (t == 1) ans = max(ans, dfs(y, cnt0, cnt1+1));
			else ans = min(ans, dfs(y, cnt0+1, cnt1));
		}
		else
		{
			if (t == 1) ans = max(ans, dfs(y, cnt0+1, cnt1)); // 1100100
			else ans = min(ans, dfs(y, cnt0, cnt1+1));
		}
	}

	if (leaf) ans = (double)cnt1/(cnt0+cnt1);
	else if (e-1) ans = min(ans, (double)cnt1/(cnt0+cnt1));
	return ans;
}

int T = next();
while(T--)
{
	int n = next();

	tot = 0;
	rep(i, 0, n+1) head[i] = vis[i] = 0;

	rep(i, 0, n-1)
	{
		int u, v, k;
		cin>>u>>v>>k;
		add(u, v, k);
		add(v, u, k);
	}

	cout<<fixed<<setprecision(12)<<dfs(1, 0, 0)<<endl;
}


B - Cake 2

给定正 \(n\) 边形,连接所有距离(两点之间最小边数)为 \(k\) 的顶点,求多边形内部被划分为多少区域。
\(n \in [4, 1e6], k \in [2, n-2]\)


画图可以得知:若 \(2k = n\),则答案为 \(n\);否则答案为\(n*min\{k, n-k\}+1\)
证明直接咕了。


int n, k;
cin>>n>>k;
if (2*k == n) cout<<n<<endl;
else
{
    if (k > n/2) cout<<n*(n-k)+1<<endl;
    else cout<<n*k+1<<endl;
}


C - Cake 3

怎么是记几构造啊/jk



D - Puzzle: Wagiri

给定一张简单连同无向图,边有两种状态LunQie
现要求移除任意一些边使得图连通,同时Lun边都在环上,Qie边都不在环上,输出任意方案或无解。
\(n \leq 1e5, m \in [n-1, 2e5]\)


显然如果对一个图求e-DCC即边双连通分量,所有的边要么在一个e-DCC中要么作为桥。
因此可以忽略所有Qie边,对仅有Lun边的图求e-DCC,得出的所有桥删除后就能够满足所有Lun边都在环上的条件。
然后考虑图联通这一条件,显然需要恢复几条Qie边使各个完全不连通的e-DCC连通。于是处理出所有Qie边连接的两个e-DCC,并使用并查集逐步尝试恢复Qie边使图连通。因为并查集具有树状结构,所以保证了Qie边都不在环上。


const int U = 5e5;
int head[U], to[U], nxt[U], stat[U], tot = 1;
void add(int x, int y, int st)
{
	to[++tot] = y; nxt[tot] = head[x]; head[x] = tot; stat[tot] = st;
}
int dfn[U], low[U], n, m, ver;
bool bridge[U];
void tarjan(int e, int ine) // 求e-DCC
{
	dfn[e] = low[e] = ++ver;
	for(int g = head[e]; g; g = nxt[g])
	{
		if (stat[g]) continue;
		int y = to[g], cc = 0;
		if (!dfn[y])
		{
			tarjan(y, g);
			low[e] = min(low[e], low[y]);

			if (dfn[e] < low[y]) bridge[g] = bridge[g^1] = 1;
		}

		else if (g != (ine^1)) low[e] = min(low[e], dfn[y]);
	}
}
int col[U], dcc;
void dfs(int e) // 编号e-DCC
{
	col[e] = dcc;
	for(int g = head[e]; g; g = nxt[g])
	{	
		int y = to[g];
		if (col[y] || stat[g] || bridge[g]) continue;
		dfs(y);
	}
}
int father[U]; // 并查集
int find(int k)
{
	if (father[k] == k) return k;
	return father[k] = find(father[k]);
}
void uni(int a, int b)
{
	father[find(a)] = find(b);
}

cin>>n>>m;
rep(i, 0, m)
{
	int u, v; string s;
	cin>>u>>v>>s;
	if (s[0] == 'L') add(u, v, 0), add(v, u, 0);
	else add(u, v, 1), add(v, u, 1);
}

hrp(i, 1, n) if (!dfn[i]) tarjan(i, 0);
hrp(i, 1, n) if (!col[i]) dcc++, dfs(i);

hrp(i, 0, dcc) father[i] = i;

set<pii> ans;
hrp(i, 1, n) for(int g = head[i]; g; g = nxt[g])
{
	int y = to[g];
	if (i > y) continue; // 由于建了双向边所以忽略一个方向的边

	if (!stat[g] && !bridge[g]) ans.insert({i, y}); // Lun边不是桥
	else if (stat[g] && find(col[i]) != find(col[y])) // Qie边作为桥
	{
		ans.insert({i, y});
		uni(col[i], col[y]);
	}
}

bool yes = 1;
hrp(i, 2, dcc) if (find(i-1) != find(i)) yes = 0;

if (!yes) cout<<"NO"<<endl;
else
{
	cout<<"YES"<<endl<<ans.size()<<endl;
	for(auto p:ans) cout<<p.first<<' '<<p.second<<endl;
}


E - Palindrome Counter

求所有长为 \(n\),字符集个数为 \(k\) 的字符串的本质不同回文子串长度之和。



F - Challenge NPC 2

(随机算法大失败)
给定一棵森林,求其补图的Hamilton路径。
\(n \leq 5e5\)


首先如果该森林为菊花,也就是补图中有一个点度数为 \(0\),显然不可能有解。
然后考虑直接对该森林进行分析。对其中一棵树从任意节点开始求深度,能够发现由于树的性质:

  1. 相同深度的节点间没有边
  2. 只有相邻深度的节点间可能有边

于是在补图上存在性质:如果两个节点深度不相邻,那么它们之间一定有边。
同时由于原图上树与树之间不连通,补图上两棵树的任意节点间都存在边。于是可以令:一棵树的起始节点深度为上一颗树深度最大值+1。这样可以获得编号 \(1 \sim m\) 的若干节点。由于补图性质,可以构造出2->4->...->1->3->...的顺序使所有点都连通起来。如果 \(m < 4\),则需要特殊判定。
因为从任意的节点开始求深度并不总能获得深度的最大值,所以最终得出的总深度 \(m\) 可能小于 \(4\),从而造成错误的无解。于是应该对每棵树都从直径的一端开始求深度,从而能保证 \(m\) 取得最大,这样 \(m < 4\) 的情况只在 \(n < 4\) 时出现。


const int U = 2e6;
int tot, head[U], nxt[U], to[U];
void add(int x, int y)
{
	to[++tot] = y; nxt[tot] = head[x]; head[x] = tot;
}
int dep[U];
vector<int> vd[U];

int T;
cin>>T;
while(T--)
{
	int n, m;
	cin>>n>>m;
	hrp(i, 0, 3*n) head[i] = dep[i] = 0;
	
	rep(i, 0, m)
	{
		int u, v; cin>>u>>v;
		add(u, v); add(v, u);
	}

	if (n == 2)
	{
		if (m) cout<<-1<<endl;
		else cout<<"1 2"<<endl;
		continue;
	}
	if (n == 3)
	{
		if (m == 2) cout<<-1<<endl;
		else if (m == 1)
		{
			int t;
			hrp(i, 1, n) if (!head[i]) t = i;
			if (t == 2) cout<<"1 2 3"<<endl;
			else if (t == 1) cout<<"2 1 3"<<endl;
			else cout<<"1 3 2"<<endl;
		}
		else cout<<"1 2 3"<<endl;
		continue;
	}

	int maxd = 0;
	hrp(i, 1, n) if (!dep[i])
	{
		queue<pii> que; que.push({i, 0});
		int s, e;
		while(que.size())
		{
			auto [x, fa] = que.front(); que.pop();
			s = x;
			for(int g = head[x]; g; g = nxt[g])
			{
				int y = to[g];
				if (y != fa) que.push({y, x});
			}
		}

		que.push({s, ++maxd});
		while(que.size())
		{
			auto [x, d] = que.front(); que.pop();
			dep[x] = maxd = d;

			for(int g = head[x]; g; g = nxt[g])
			{
				int y = to[g];
				if (!dep[y]) que.push({y, d+1});
			}
		}
	}

	hrp(i, 0, maxd) vd[i].clear();
	hrp(i, 1, n) vd[dep[i]].pb(i);

	if (maxd < 4) cout<<-1<<endl;
	else
	{
		for(int d = 2; d <= maxd; d += 2) for(auto i:vd[d]) cout<<i<<' ';
		for(int d = 1; d <= maxd; d += 2) for(auto i:vd[d]) cout<<i<<' ';
		cout<<endl;
	}
}


G - Easy Brackets Problem

/?



H - Genshin Impact's Fault

模拟Genshin抽卡,卡分为3星,4星,A5星,B5星4种。每10抽不能只有3星,每90抽至少有1个5星,抽到的5星必须AB交替。


int T = next();
while(T--)
{
	string str = next<string>();

	bool no = 0;
	if (str.size() >= 10) hrp(i, 0, str.size()-10)
	{
		bool all3 = 1;
		rep(j, i, i+10) if (str[j] != '3') all3 = 0;
		if (all3)
		{
			no = 1;
			break;
		}
	}

	if (str.size() >= 90) hrp(i, 0, str.size()-90)
	{
		bool no5 = 1;
		rep(j, i, i+90) if (str[j] == '5' || str[j] == 'U') no5 = 0;
		if (no5)
		{
			no = 1;
			break;
		}
	}

	string s5;
	rep(i, 0, str.size()) if (str[i] == '5' || str[i] == 'U') s5 += str[i];
	if (s5.size()) rep(i, 0, s5.size()-1) if (s5[i] != 'U' && s5[i+1] != 'U') no = 1;

	if (no) cout<<"invalid"<<endl;
	else cout<<"valid"<<endl;
}


I - Intersecting Intervals

给定一个数字矩阵,每一行选择一个区间,相邻行区间必须有交,求选中数字和的最大值。
\(n*m \leq 1e6\)


一眼dp,很容易想出状态dp(i, j)代表dp在第i行选了j后的最大值
有一个显然的转移枚举该行 \(k\),强制该行选 \(j \sim k\),但 \(j\) 前和 \(k\) 后的数字还可能被选使答案更大。因此用O(m)复杂度预处理出每行的pre(k), suf(k),分别代表包含 \(k\)\(k\) 前和 \(k\) 后一些序列的最大贡献。
于是有:
dp(i, k) = dp(i-1, j)+sum(j...k)+pre(j)+suf(k)(j<k)
dp(i, k) = dp(i-1, j)+sum(k...j)+pre(k)+suf(j)(j>k)
从而得出了O(mn^2)的转移(其中sum, pre, suf数组均右移了1位):

rep(j, 0, m) hrp(k, 0, j) dp[i][j] = max(dp[i][j], dp[i-1][k]+sum[j+1]-sum[k]+pre[k]+suf[j+2]);
rep(j, 0, m) rev(k, m-1, j) dp[i][j] = max(dp[i][j], dp[i-1][k]+sum[k+1]-sum[j]+pre[j]+suf[k+2]);

复杂度不够,于是需要优化,有两种办法(仅说明 \(j < k\) 情况):

  1. 关注定值:能够发现sum[j+1]+suf[j+2]为定值,于是提出定值并在转移 \(j\) 的时候保存已经遍历过的dp[i-1][k]+sum[k]+pre[k]的最大值。
  2. 关注转移 \(j\) 过程:设V=dp[i-1][p]+sum[p+1]-sum[k]+pre[k]+suf[p+2]\(j = 1 \sim p\) 时包含所有 \(V\) 取值的集合为 \(S_p\) 。能够发现V'=dp[i-1][p]+sum[p+2]-sum[k]+pre[k]+suf[p+3]=V+sum[p+1]-sum[p]+suf[p+3]-suf[p+2]=V+w[p+1]+suf[p+3]-suf[p+2],从而让所有 \(V'\)dp[i-1][p+1]+v[i][p+1]+pre[p+1]+suf[p+3]一起构成了\(S_{p+1}\)

代码采用的是优化2:

int T = next();
while(T--)
{
	v.clear();
	dp.clear();

	int n, m;
	cin>>n>>m;
	rep(i, 0, n+5) v.pb(vector<int>()), dp.pb(vector<int>());

	hrp(i, 1, n) rep(j, 0, m) v[i].pb(next());
	rep(i, 0, n+5) rep(j, 0, m+5) dp[i].pb(i ? -INF : 0);

	hrp(i, 1, n)
	{
		vector<int> sum{0}, pre{0}, suf{0};

		rep(j, 0, m) sum.pb(sum.back()+v[i][j]);
		rep(j, 0, m) pre.pb(max(pre.back()+v[i][j], 0LL));
		rev(j, m-1, 0) suf.pb(max(suf.back()+v[i][j], 0LL));
		reverse(suf.begin(), suf.end());
		suf.insert(suf.begin(), 0);
		pre.insert(pre.end(), 0);

		int maxx = -INF;
		rep(j, 0, m)
		{
			maxx += v[i][j]-suf[j+1]+suf[j+2];
			maxx = max(maxx, dp[i-1][j]+v[i][j]+pre[j]+suf[j+2]);
			dp[i][j] = maxx;
		}
		maxx = -INF;
		rev(j, m-1, 0)
		{
			maxx += v[i][j]-pre[j+1]+pre[j];
			maxx = max(maxx, dp[i-1][j]+v[i][j]+pre[j]+suf[j+2]);
			dp[i][j] = max(dp[i][j], maxx);
		}
	}

	int ans = -INF;
	rep(j, 0, m) ans = max(ans, dp[n][j]);
	cout<<ans<<endl;
}

优化1:

int maxx = -INF;
rep(j, 0, m)
{
	maxx = max(maxx, -sum[j]+pre[j]+dp[i-1][j]);
	dp[i][j] = max(dp[i][j], maxx+sum[j+1]+suf[j+2]);
}

maxx = -INF;
rev(j, m-1, 0)
{
	maxx = max(maxx, sum[j+1]+suf[j+2]+dp[i-1][j]);
	dp[i][j] = max(dp[i][j], maxx-sum[j]+pre[j]);
}


J - Stone Merging

\(n\) 堆石子,开始时每堆 \(1\) 个,每个石子被可重复编号。有 \(k-1\) 个机器编号为 \(2 \sim k\)
编号为 \(g\) 的机器可以合并 \(g\) 堆石子;但如果这 \(g\) 堆石子中有至少 \(1\) 个编号为 \(g\) 的石子,该编号为 \(g\) 的机器会不能再使用,并将所有编号为 \(g\) 的石子提取出并放成一堆,把剩下的石子放成另一堆。
输出将所有石子合成一堆的方案或无解。
\(1 \leq k \leq n \leq 1e5\)


除了仅有 \(2\) 堆编号为 \(2\) 的石子的情况,\(2 \sim k\)的编号都在石子上出现过是无解的。这种情况先进行特判。
首先编号为 \(2\) 的机器人一定存在,而且功能十分强大,能够将所有非 \(2\) 石子都合成一堆。
讨论:

  1. 只存在编号为 \(2\) 的机器人。由于特判过,所以不可能存在编号为\(2\)的石子。全都合并即可。
  2. 存在编号为 \(t\) 的机器人,使得编号为 \(t\) 的石头不存在。所以当前的任务为把石子堆数从 \(n\) 减少到 \(t\)(官方题解采用方法不同)。因为 \(3\) 号机器人存在,所以我们可以用 \(3\) 号机器人把编号为 \(2\) 的石子堆成双减少;同时用 \(2\) 号机器人把编号不为 \(2\) 的石子堆逐个减少。此外还可以在最后使用 \(2\) 号机器人把编号为 \(2\) 的石子合并为 \(1\) 堆。如果石子堆个数无法达到 \(t\),则无解(事实上一定有解)。

使用set实现而非vector来降低删除操作的时间复杂度。


官方题解的方法为第一步将\(y=(n−1)~mod~(x−1)+1\) 个石子合成成一堆, 后面每次都使用机器 \(t\) 来合成。


const int U = 1e6;
int w[U];
bool exist[U];

int T = next();
while(T--)
{
	int n, k, op = 0;
	cin>>n>>k;
	hrp(i, 1, n) exist[i] = 0;

	set<int> p2, pn2;
	vector<vector<int>> ans;

	hrp(i, 1, n)
	{
		cin>>w[i];
		if (w[i] == 2) p2.insert(i);
		else pn2.insert(i);
		exist[w[i]] = 1;
	}

	if (!pn2.size() && p2.size() == 2)
	{
		cout<<"1"<<endl<<"2 1 2"<<endl;
		continue;
	}

	int tar = 0;
	rev(i, k, 2) if (!exist[i])
	{
		tar = i;
		break;
	}

	if (!tar)
	{
		cout<<-1<<endl;
		continue;
	}

	if (k == 2) while(pn2.size() > 1)
	{
		vector<int> v{*pn2.begin(), *next(pn2.begin())};
		ans.pb(v);
		pn2.erase(pn2.begin());
		pn2.erase(pn2.begin());
		op++;
		pn2.insert(n+2*op);
	}
	else
	{
		bool mat = 0;
		if (p2.size()+pn2.size() == tar) mat = 1;
		
		while(pn2.size()+p2.size() > tar+1 && p2.size() > 2 && !mat)
		{
			vector<int> v{*p2.begin(), *next(p2.begin()), *next(next(p2.begin()))};
			ans.pb(v);
			p2.erase(p2.begin());
			p2.erase(p2.begin());
			p2.erase(p2.begin());
			op++;
			p2.insert(n+2*op);

			if (p2.size()+pn2.size() == tar) mat = 1;
		}
		
		while(pn2.size() > 1 && !mat)
		{
			vector<int> v{*pn2.begin(), *next(pn2.begin())};
			ans.pb(v);
			pn2.erase(pn2.begin());
			pn2.erase(pn2.begin());
			op++;
			pn2.insert(n+2*op);

			if (p2.size()+pn2.size() == tar) mat = 1;
		}

		if (p2.size()+pn2.size() == tar+1 && p2.size() > 1)
		{
			vector<int> v{*p2.begin(), *next(p2.begin())};
			ans.pb(v);
			p2.erase(p2.begin());
			p2.erase(p2.begin());
			op++;
			p2.insert(n+2*op-1);
			mat = 1;
		}

		if (mat)
		{
			vector<int> v;
			for(auto i:p2) v.pb(i);
			for(auto i:pn2) v.pb(i);
			ans.pb(v);
		}
		// else
		// {
		//	cout<<-1<<endl;
		//	continue;
		// }
	}

	cout<<ans.size()<<endl;
	for(auto i:ans)
	{
		cout<<i.size()<<' ';
		for(auto j:i) cout<<j<<' ';
		cout<<endl;
	}
}


K - The Great Wall 2

给定长为 \(n\) 的整数序列,要求分为恰好 \(k\) 个非空连续段使得这 \(k\) 段的极差之和最小。
要求对 \(k = 1 \sim n\) 分别求解
\(n \leq 5000\)


不会,zzz

posted @ 2024-08-05 18:10  Loxilante  阅读(43)  评论(2编辑  收藏  举报