线段树

比赛概述

A、C、D 来自小猫,B 来自 ZYZ.

验题人是小猫和 GPT5.

四道题都是小猫场切过的。小猫写四道题的时间大概分别是,10min + 50min + 2h + 1.5h.

小猫一合计,诶,这不就是一场 NOIP 模拟赛的 easy ver. 嘛!

于是就有了这场比赛。

关于难度

验题人(?)GPT5 在本场比赛里获得了 \(100+100+0+100\) 的分数,可喜可贺。

小猫认为难度大概是黄、绿、紫、蓝。

ydtz 认为是绿、绿、蓝/紫、蓝/紫。

关于算法

先声明这场不是数学 Round 或计数 Round.

感觉上 T4 相较于 counting 更偏 ds 和思维一点。

A. 航海马(sail)

航海的马与击球手散养的白鹅大战,发出的呱呱声吵醒了林中的猕猴,声音刺耳,令人心灵震荡不已,吵得猕猴赶紧戴上耳塞【数据删除】,却因违反禁止带耳机的校规被 lz 发了黄牌。猴兄驱赶不停嗷嗷的蛐蛐,又骑着永康的公牛飞上岁岁似今朝的浅云,公牛张翼翔飞,又不慎掉到了一条峻溪当中,发出了 hiphop 的水花声。峻溪十分险峻,还有千回百转的折壕,只有【数据删除】的光芒可以指引前路。猴兄根据【数据删除】的指引来到了涌溪,遇到了翼德天尊和 PYD1,两人问猴兄:“你掉的是这块夏令营银牌还是这两块 NOI 铜牌?”猴兄说:“Wishtoday!不要剥削!”

多年以后,当航海马追忆过去,这段话忽然出现在脑海。他虔诚地天上的神明祈祷,回应他的却只有他自己。

原题是 CF2042C.

考虑最终的序列是单调不降的,它可以拆成不超过 \(n\) 个后缀相加。

每个后缀之间的贡献其实是独立的。于是我们直接记 \(a_i\) 表示选择后缀 \([i, n]\) 对答案产生的贡献。贪心地取所有贡献 \(> 0\) 的后缀即可。

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

const int N = 2e5 + 5;

int n, k, suf[N];
char s[N];

void solve()
{
	cin >> n >> k;
	cin >> (s + 1);
	suf[n + 1] = 0;
	for(int i = n; i >= 1; i--)
		suf[i] = suf[i + 1] + (s[i] == '1' ? 1 : -1);
	priority_queue<int> q;
	int ans = 1, cur = 0;
	for(int i = n; i > 1; i--) q.push(suf[i]);
	while(!q.empty())
	{
		if(q.top() <= 0) break;
		cur += q.top(); q.pop();
		ans++;
		if(cur >= k) break;
	}
	if(cur < k) cout << -1 << '\n';
	else cout << ans << '\n';
	return;
}

int main()
{
	freopen("sail.in", "r", stdin);
	freopen("sail.out", "w", stdout);
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int t; cin >> t;
	while(t--) solve();
	return 0;
}

验题人评价

image

image

B. 树上旅行(tree)

#include <bits/stdc++.h>
#define ll long long
#define fi first
#define se second
#define pii pair<int,int>
#define pll pair<ll,ll>
#define ull unsigned long long
using namespace std;
const int iinf=1<<30;
const ll linf=1ll<<62;
const int P=998244353;

int n,q;
stack<array<int,2>> stk;
ll base[100011],s[100011],pos=1;

ll mpow(ll x,int y){
	if(y==0) return 1;
	if(y&1) return mpow(x*x%P,y/2)*x%P;
	return mpow(x*x%P,y/2);
}

int main(){
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(0);
	cin >> n >> q;
	base[0]=1;
	for(int i=1;i<=100000;i++)
		base[i]=base[i-1]*n%P;
	s[1]=1;
	for(int i=2;i<=100000;i++)
		s[i]=(s[i-1]+base[i-1])%P;
	while(q--){
		int x,y;
		cin >> x >> y;
		if(x!=0){
			pos=((pos*base[y]%P+(1+x-n)*s[y]%P)%P+P)%P;
			stk.push({x,y});
		}else{
			while(stk.size()&&y>=stk.top()[1]){
				pos=(pos-(1+stk.top()[0]-n)*s[stk.top()[1]]%P+P)%P;
				pos=pos*mpow(base[stk.top()[1]],P-2)%P;
				y-=stk.top()[1];
				stk.pop();
			}
			if(y){
				pos=(pos-(1+stk.top()[0]-n)*s[y]%P+P)%P;
//				cout << pos << ' ';
				pos=pos*mpow(base[y],P-2)%P;
//				cout << pos << ' ';
				array<int,2> t=stk.top();
				t[1]-=y;
				stk.pop();
				stk.push(t);
			}
		}
		cout << pos << '\n';
	}
	return 0;
}

验题人评价

image
image

C. 好朋友(npy)

首先不难发现序列的 LIS 和 LDS 恰有一个交点,且交点位置一定是 \(a_{n}\). 发现 \(n\) 前后是独立的,可以分别计算。

对前半部分,相当于要在 \([1, n-1]\) 填入 \(1, 2, \cdots a_{n}-1\) 这个升序序列与 \(n, n-1, \cdots, a_{n} + 1\) 这个降序序列。后半部分同理。

注意到 B 性质是有用的,它相当于左半部分和中间的值唯一确定了,只需考虑右半部分。然后我们的正解其实就是 B 性质的做法分别对两边做,然后在中间合并。

Solution B-1

B 性质,左半部分和中间的值唯一确定了,只需考虑右半部分。

\(f_{i, j, k}\) 表示后半部分从后往前填完前 \(i\) 个位置,升序序列填到 \(j\),降序序列填到 \(k\) 的方案数. 可以得到 \(0\) 分。

Solution 1

根据 Sol B-1 不难想到两半都这样 DP,然后从中间拼起来。可以得到 \(28\) 分。

Solution B-2

注意到之前做法中第三维状态是没用的(即可以根据前两维状态唯一确定第三维)。

于是考虑 \(f_{i, j}\) 表示前半部分填完前 \(i\) 个位置,当前升序序列填到了 \(j\) 的方案数。转移如下:

\[\begin{cases} f_{i, j} \gets f_{i-1,j} + f_{i-1,j - 1}, & a_i = 0,\\ f_{i, j} \gets f_{i-1,j-1}, &a_i=j,\\ f_{i, j} \gets f_{i-1,j}, &a_i=n-(i-j)+1.\\ \end{cases} \]

Solution 2

考虑最终的答案。

  • 如果 \(a_n=0\),则答案是 \(\displaystyle \sum_{i=0}^{n-1} f_{n-1,i} \times g_{n+1,i}\).
  • 否则,答案是 \(f_{n-1,a_n-1} \times g_{n+1,a_n+1}\).

这样可以获得 \(52\) 分。

Solution A

A 性质,容易发现 \(a_i = 0\) 时式子变成了组合数。\(O(n)\) 预处理阶乘和阶乘逆元即可。

和 Sol2 拼起来可以获得 \(56\) 分。

Solution B-3

继续思考,在做 A 性质的时候我们利用了 \(a_i = 0\) 时转移式变成组合数的性质。进一步可以发现,如果当前位置是 \(i\),上一个 \(a_i \ne 0\) 的位置是 \(i'\),则 \(f_{i', j'} \to f_{i, j}\) 的贡献就是 \(\displaystyle {i-i' \choose j-j'} \times f_{i', j'}\).

而我们可以注意到 \(a_i \ne 0\) 时,值非零的 \(f_{i, j}\) 其实只有 \(2\) 个。于是我们存下每个 \(i\) 对应的这两个 \(j\),然后暴力转移即可。

Solution 3

就是 Sol B-3 拼起来,没什么好说的。

可以获得 \(100\) 分。

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

const int mod = 998244853, M = 1e6 + 5;
int n, a[M * 2], f[M], g[M], b[M], fac[M], inv[M], ls, h[M][2], s[M][2];

inline int ksm(int x, int y)
{
	int res = 1;
	while(y)
	{
		if(y & 1) res = 1ll * res * x % mod;
		x = 1ll * x * x % mod, y >>= 1;
	}
	return res;
}

int C(int n, int m)
{
	if(n < 0 || m < 0 || n < m) return 0;
	return 1ll * fac[n] * inv[m] % mod * inv[n - m] % mod;
}

int calc(int ls, int p, int x)
{
	int res = 0;
	for(int o = 0; o <= 1; o++)
	{
		if(h[ls][o] == -1) continue;
		res = (res + 1ll * s[ls][o] * C(p - ls, x - h[ls][o]) % mod) % mod;
	}
	return res;
}

void solve()
{
	cin >> n;
	for(int i = 1; i <= 2 * n - 1; i++) cin >> a[i];
	for(int i = 1; i <= n; i++) b[i] = a[2 * n - i];
	for(int j = 0; j <= n; j++) f[j] = g[j] = 0;
	for(int i = 1; i <= n; i++) h[i][0] = h[i][1] = -1, s[i][0] = s[i][1] = 0;
	h[0][0] = 0, s[0][0] = 1, ls = 0;
	for(int i = 1; i <= n - 1; i++)
	{
		if(a[i])
		{
			if(a[i] > 0)
			{
				int s1 = calc(ls, i - 1, a[i] - 1); // ls 贡献到 f[i - 1][a[i] - 1];
				if(s1) h[i][0] = a[i], s[i][0] = s1;
			}
			if(i >= n - a[i] + 1)
			{
				if(i - (n - a[i] + 1) < i)
				{
					int s2 = calc(ls, i - 1, i - (n - a[i] + 1));
					if(h[i][0] != -1 && h[i][0] != i - (n - a[i] + 1) && s[i][0] != 0) h[i][1] = i - (n - a[i] + 1), s[i][1] = s2;
					else h[i][0] = i - (n - a[i] + 1), s[i][0] += s2, s[i][0] %= mod;
				}
			}
			ls = i;
		}
	}
	for(int i = 0; i <= n - 1; i++) f[i] = calc(ls, n - 1, i);
	for(int i = 1; i <= n; i++) h[i][0] = h[i][1] = -1, s[i][0] = s[i][1] = 0;
	h[0][0] = 0, s[0][0] = 1, ls = 0;
	for(int i = 1; i <= n - 1; i++)
	{
		if(b[i])
		{
			if(b[i] > 0)
			{
				int s1 = calc(ls, i - 1, b[i] - 1); // ls 贡献到 g[i - 1][b[i] - 1];
				if(s1) h[i][0] = b[i], s[i][0] = s1;
			}
			if(i >= n - b[i] + 1)
			{
				if(i - (n - b[i] + 1) < i)
				{
					int s2 = calc(ls, i - 1, i - (n - b[i] + 1));
					if(h[i][0] != -1 && h[i][0] != i - (n - b[i] + 1) && s[i][0] != 0) h[i][1] = i - (n - b[i] + 1), s[i][1] = s2;
					else h[i][0] = i - (n - b[i] + 1), s[i][0] += s2, s[i][0] %= mod;
				}
			}
			ls = i;
		}
	}
	int ans = 0;
	for(int i = 0; i <= n - 1; i++) g[i] = calc(ls, n - 1, i);
	if(a[n]) cout << 1ll * f[a[n] - 1] * g[a[n] - 1] % mod << '\n';
	else
	{
		for(int i = 0; i <= n - 1; i++)
			ans += 1ll * f[i] * g[i] % mod, ans %= mod;
		cout << ans << '\n';
	}
	return;
}

int main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	freopen("npy.in", "r", stdin);
	freopen("npy.out", "w", stdout);
	fac[0] = 1;
	for(int i = 1; i <= 1e6; i++)
		fac[i] = 1ll * fac[i - 1] * i % mod;
	inv[(int) 1e6] = ksm(fac[(int) 1e6], mod - 2);
	for(int i = 1e6; i >= 1; i--)
		inv[i - 1] = 1ll * inv[i] * i % mod;
	int t; cin >> t;
	while(t--) solve();
	return 0;
}

验题人评价

image

D. 蟠桃(peach)

喜怒哀乐都是我的心情

每站有不同浪漫风景

该走了 我和她有个约定

Solution #1

\(n \le 300\) 的部分可以直接暴力枚举区间 \([l, r]\),然后直接按照题意判断。

可以获得 \(30\) 分。

Solution #2

\(L_i, R_i\) 表示 \(i\) 的所有出边与 \(i\) 自身中连到的下标的最小、最大值。

注意到若一个区间包含下标 \(i\),则它必然包含 \([L_i, R_i]\).

先枚举 \(L\),然后在枚举 \(R\) 的过程中动态维护 \(\min L_i\)\(\max R_i\) 即可。

时间复杂度 \(O(n^2)\),可以获得 \(45\) 分。

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

const int N = 5e5 + 5, M = 2e6 + 5;
int c, n, m, x, y, l[M], r[M];
long long ans;

signed main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	freopen("peach.in", "r", stdin);
	freopen("peach.out", "w", stdout);
	cin >> c >> n >> m;
	for(int i = 1; i <= n; i++) l[i] = r[i] = i;
	for(int i = 1; i <= m; i++)
	{
		cin >> x >> y;
		l[x] = min(l[x], y);
		r[x] = max(r[x], y);
	}
	for(int i = 1; i <= n; i++)
	{
		int ll = n, rr = 0;
		for(int j = i; j <= n; j++)
		{
			ll = min(ll, l[j]);
			rr = max(rr, r[j]);
			if(ll < i) break;
			if(rr > j) continue;
			ans++;
		}
	}
	cout << ans;
	return 0;
}

Solution #3

考虑在固定 \(l\) 枚举 \(r\) 的过程中,\(\min L_i\)\(\max R_i\)(即上面代码中的 ll, rr)变化的次数至多是 \(O(\min(n, m))\) 次,而发生变化的位置只可能是有连边的点。这些点将序列分成了 \(O(m)\) 段,每段中对答案产生贡献的都是一段后缀,可以直接算出来。

时间复杂度 \(O(nm)\),实现劣的话会变成 \(O(nm \log n)\),应该也能过。

可以通过 \(m=100\) 的部分分.

Solution #4

考虑统计不合法的段,然后使用广义 HXF 容斥(即整体 - 不合法)计算合法答案。

仍然考虑合法等价于 \(\min L_i \ge l\),且 \(\max R_i \le r\). 由于 \(|x-y|\le 20\),对每个 \(l\),可能存在 \(L_i < l\) 的位置一定在 \([l, l+20]\) 内。\(r\) 的情况同理。

先暴力计算区间长度 \(\le 20\) 的答案。

对于区间长度 \(>20\) 的,我们考虑用左端点匹配右端点。一个左端点 \(l'\) 和右端点 \(r'\) 能匹配,当且仅当 \([l', l'+20]\)\([r'-20, r']\) 都是“安全”的。直接做就好了。

Solution #5

我们要再次把区间合法的条件拉出来:\(\displaystyle \min_{i=l}^r L_i \ge i\),且 \(\displaystyle \max_{i=l}^r R_i \le i\).

我们尝试把条件分开处理。

对一个固定的 \(r\),满足第二个条件的 \(l\) 显然是一段后缀,可以二分得到。现在我们只需要计算一个有多少 \(l \in [p, r]\) 满足 \(\displaystyle \min_{i=l}^r L_i \ge i\).

有一个小性质:合法区间一定满足 \(L_l = l\), \(R_r = r\),证明显然。

考虑什么情况下一个左端点会不合法。显然如果 \(l, r\) 之间有一个位置 \(q\) 使得 \(L_q < l\),它就不合法了。于是我们维护单调栈,每遍历到一个右端点 \(r\) 就弹出所有 \(> L_r\) 的栈顶。如果 \(L_r = l\),就将其插入。

每次查询就是询问单调栈内有多少个 \([p, r]\) 之间的元素,二分即可。

有人场上写了 4k 线段树优化建图后缩点跑 dag 上 DP,然后才发现 DP 后得到的东西和不 DP 前一样。我不说是谁。

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

const int N = 5e5 + 5, M = 2e6 + 5;
int c, n, m, x, y, l[M], r[M], lt[M], lg[N];
long long ans;
int head[M], tot, fl[N][21], fr[N][21];
bool inst[M], vis[M];
stack<int> stk;

struct edge
{
	int to, nxt;
} e[M << 2];

void addedge(int u, int v)
{
	e[++tot] = {v, head[u]};
	head[u] = tot;
}
int calc_fl(int l, int r)
{
	int x = lg[r - l + 1];
	return min(fl[l][x], fl[r - (1 << x) + 1][x]);
}

int calc_fr(int l, int r)
{
	int x = lg[r - l + 1];
	return max(fr[l][x], fr[r - (1 << x) + 1][x]);
}

int stk2[M], top;

signed main()
{
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	freopen("peach.in", "r", stdin);
	freopen("peach.out", "w", stdout);
	cin >> c >> n >> m;
	for(int i = 1; i <= n; i++) l[i] = r[i] = i;
	for(int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
	for(int i = 1; i <= m; i++)
	{
		cin >> x >> y;
		l[x] = min(l[x], y);
		r[x] = max(r[x], y);
	}
	for(int i = 1; i <= n; i++)
		fl[i][0] = l[i], fr[i][0] = r[i];
	for(int j = 1; j <= 20; j++)
	for(int i = 1; i + (1 << j) - 1 <= n; i++)
	{
		fl[i][j] = min(fl[i][j - 1], fl[i + (1 << (j - 1))][j - 1]);
		fr[i][j] = max(fr[i][j - 1], fr[i + (1 << (j - 1))][j - 1]);
	}
	for(int i = 1; i <= n; i++)
	{
		while(top && stk2[top] > l[i]) top--;
		if(l[i] == i) stk2[++top] = l[i];
		if(i != r[i]) continue;
		int L = 1, R = i, p = i;
		while(L <= R)
		{
			int mid = (L + R) >> 1;
			if(calc_fr(mid, i) <= i) p = mid, R = mid - 1;
			else L = mid + 1;
		}
		L = 1, R = top; int q = top + 1;
		while(L <= R)
		{
			int mid = (L + R) >> 1;
			if(stk2[mid] >= p) q = mid, R = mid - 1;
			else L = mid + 1;
		}
		ans += top - q + 1;
	}
	cout << ans;
	return 0;
}

验题人评价

image

posted @ 2024-07-22 08:23  心灵震荡  阅读(24)  评论(0)    收藏  举报