2023年省选记录

Day 1 3/13

昨天晚上没睡好,今天状态很差。晚上19:15开始做题,预计做两道。

19:40开始看第一题题解。没有想到任何关键点上,我是傻逼。接下来睡了一会。20:15开始写。

就写了一道,开颓,埋了。

联合省选2022 填树

给定一棵 \(n\) 个点的无根树,对每个点 \(i\) 给定一个区间 \([l_i, r_i]\),求对于所有树上路径,如果将这个路径上每一个点填上其对应区间中的一个数,且最终所有数极差小于等于 \(K\),那么共有多少种方案,以及这些方案中填上的数的总和。

数据范围:\(1 \le n \le 200, 1 \le l_i \le r_i \le 10^9, 1 \le K \le 10^9\)

可以说是我做的第二道拉格朗日插值优化DP题,就从第一步开始想歪了/kk/kk。

首先,如果已经确定路径上数的最小值,那么可以非常容易地计算出任何一个路径的权值和。具体地,暴力DP时间复杂度为 \(\Theta(n^2)\),换根DP优化即可做到 \(\Theta(n)\)。则直接枚举最小值,总复杂度是 \(\Theta(nW)\),其中 \(W\) 是值域。

接下来我们需要考虑把 \(W\) 优化成 \(n\)。这样想的直觉是,除了区间端点以外的其他值都很相似,并且不太重要。考虑最小值为 \(L\) 时,点 \(i\) 可以取的区间是 \([l_i, r_i] \cap [L, L + K]\),当 \(L\) 增加 1 时,如果它或者对应右端点没有触碰到某个 \(l_i, r_i\),则方案总数可以表示为一堆一次函数相乘然后再相加,而第二问的方案总数可以表示为一堆二次函数相乘相加。这个东西次数是 \(\mathrm O(n)\) 的,要计算区间和,则只要拉格朗日插值求出前缀和即可。

于是做完了。考虑在端点触碰到某些关键点的时候暴力重新算出下面 \(\mathrm O(n)\) 个东西,剩下的部分用插值算一下,时间复杂度是 \(\mathrm O(n ^ 3)\),随便过。

被卡常了,提示我们不要乱堆递归,这玩意可以直接转移而不用换根的两次dfs。

// Author: kyEEcccccc

#include <bits/stdc++.h>

using namespace std;

using LL = long long;
using ULL = unsigned long long;

#define F(i, l, r) for (int i = (l); i <= (r); ++i)
#define FF(i, r, l) for (int i = (r); i >= (l); --i)
#define MAX(a, b) ((a) = max((a), (b)))
#define MIN(a, b) ((a) = min((a), (b)))
#define SZ(a) ((int)(a).size() - 1)

const int N = 205, MOD = 1000000007;

LL kpow(LL x, LL k = MOD - 2)
{
	x = x % MOD;
	LL r = 1;
	while (k)
	{
		if (k & 1) r = r * x % MOD;
		x = x * x % MOD;
		k >>= 1;
	}
	return r;
}

int n, K;
vector<int> to[N];
LL l[N], r[N], f[N], g[N], ff[N], gg[N];

void dp(int u, int par, LL L)
{
	f[u] = 1, ff[u] = 0;
	for (int v : to[u]) if (v != par)
		dp(v, u, L), f[u] += f[v], ff[u] += ff[v];
	LL ll = max(l[u], L), rr = min(r[u], L + K), x = max(rr - ll + 1, 0LL);
	f[u] %= MOD, ff[u] %= MOD;
	ff[u] = x == 0 ? 0 : (ff[u] * x % MOD + (ll + rr) * x / 2 % MOD * f[u]) % MOD;
	f[u] = f[u] * x % MOD;
}

void dp2(int u, int par, LL L)
{
	g[u] = f[u], gg[u] = ff[u];
	if (par != 0)
	{
		LL ll = max(l[u], L), rr = min(r[u], L + K);
		LL lp = max(l[par], L), rp = min(r[par], L + K);
		LL x = max(rr - ll + 1, 0LL), y = max(rp - lp + 1, 0LL);

		(g[u] += (g[par] - y * f[u] % MOD + MOD) * x) %= MOD;
		(gg[u] += (gg[par] - y * ff[u] % MOD
			- (rp + lp) * y / 2 % MOD * f[u] % MOD + 2 * MOD) * x) %= MOD;
		(gg[u] += (g[par] - y * f[u] % MOD + MOD)
			* ((rr + ll) * x / 2 % MOD)) %= MOD;
	}
	for (int v : to[u]) if (v != par) dp2(v, u, L);
}

pair<LL, LL> calc(LL L)
{
	LL res = 0, res2 = 0;
	dp(1, 0, L), dp2(1, 0, L);
	F(i, 1, n) res += g[i], res2 += gg[i];
	--K, dp(1, 0, L + 1), dp2(1, 0, L + 1), ++K;
	F(i, 1, n) res -= g[i], res2 -= gg[i];
	res = (res % MOD + MOD) % MOD;
	res2 = (res2 % MOD + MOD) % MOD;

	LL tot = 0, tot2 = 0;
	F(i, 1, n) if (l[i] <= L && L <= r[i]) tot += 1, tot2 += L;
	tot %= MOD, tot2 %= MOD;

	return {((res - tot + MOD) * (MOD + 1 >> 1) % MOD + tot) % MOD,
		((res2 - tot2 + MOD) * (MOD + 1 >> 1) % MOD + tot2) % MOD};
}

LL inter(const vector<pair<LL, LL>> &pts, LL x)
{
	LL ans = 0;
	F(i, 0, SZ(pts))
	{
		LL t = 1;
		F(j, 0, SZ(pts)) if (j != i)
			t = t * (pts[i].first - pts[j].first) % MOD;
		t = kpow(t + MOD) * pts[i].second % MOD;
		F(j, 0, SZ(pts)) if (j != i)
			t = t * (x - pts[j].first) % MOD;
		ans += t;
	}
	return (ans % MOD + MOD) % MOD;
}

int main(void)
{
	freopen("tree.in", "r", stdin);
	freopen("tree.out", "w", stdout);
	ios::sync_with_stdio(0), cin.tie(nullptr);

	cin >> n >> K;
	vector<int> imp;
	imp.push_back(1); imp.push_back(1000000001);
	F(i, 1, n)
	{
		cin >> l[i] >> r[i];
		imp.push_back(l[i]);
		if (l[i] > K) imp.push_back(l[i] - K);
		imp.push_back(r[i] + 1);
		if (r[i] > K - 1) imp.push_back(r[i] - K + 1);
	}
	sort(imp.begin(), imp.end());
	imp.resize(unique(imp.begin(), imp.end()) - imp.begin());
	F(i, 1, n - 1)
	{
		int u, v; cin >> u >> v;
		to[u].push_back(v), to[v].push_back(u);
	}
	LL ans = 0, ans2 = 0;
	#if 0
	F(i, 1, 200000) { auto pi = calc(i); ans += pi.first, ans2 += pi.second; }
	#else
	int mx = n + 2;
	F(i, 0, SZ(imp) - 1)
	{
		if (imp[i + 1] <= imp[i] + mx)
		{
			F(j, imp[i], imp[i + 1] - 1)
			{
				auto pi = calc(j);
				ans += pi.first, ans2 += pi.second;
			}
			continue;
		}

		vector<pair<LL, LL>> pts, pts2;
		LL sum = 0, sum2 = 0;
		F(j, imp[i], imp[i] + mx)
		{
			auto pi = calc(j);
			(sum += pi.first) %= MOD, (sum2 += pi.second) %= MOD;
			pts.emplace_back(j, sum), pts2.emplace_back(j, sum2);
		}
		ans += inter(pts, imp[i + 1] - 1), ans2 += inter(pts2, imp[i + 1] - 1);
		ans %= MOD, ans2 %= MOD;
	}
	#endif
	cout << ans % MOD << '\n' << ans2 % MOD << '\n';

	return 0;
}

Day 5 3/17

前几天开摆了,然后生病两天,今天接着写。叶老师说:做省选题不做FJOI,一开就是一道FJOI,还是做了……

FJOI2016 建筑师

给定一个排列,一个元素如果是前缀最大,那么可以从左边看到,如果是后缀最大,那么可以从右边看到。现有 \(T\) 组询问,每次给定 \(n, A, B\) 表示长度为 \(n\) 的排列,从左边和右边分别可以看到 \(A\)\(B\) 个元素,求方案数。

数据范围:\(1\le n \le 50000, 1 \le A, B \le 100, 1 \le T \le 200000\)

考虑解的结构,一定是以最大的数为分水岭,左边有 \(A - 1\) 个,右边有 \(B - 1\) 个合法段;其中一个合法的段指一个最大数放在头(尾)部,其它数任意排列形成的连续子序列。面对这样的结构,我们可以考虑,如果已经确定了这些段各自的形状,那么它们的拼接方式是很容易决定的:从 \(A + B - 2\) 个段中选出 \(A - 1\) 个放在左边,剩下的放在右边,并且左右分别按最大值升序和降序排列,最后把最大数放到中间。结合斯特林数的定义得到:答案为 \({n - 1 \brack A + B - 2}{A + B - 2 \choose A - 1}\)

启示我们观察出一些限制以后,先满足比较复杂的限制,再去解决比较简单的限制。比如这道题中,选择最大元和排列合法段都是比较容易满足的,计算也很方便,但是分配其它数比较麻烦,我们考虑先分配完其他数以后,就可以直接确定排列方案数。

// Author: kyEEcccccc

#include <bits/stdc++.h>

using namespace std;

using LL = long long;
using ULL = unsigned long long;

#define F(i, l, r) for (int i = (l); i <= (r); ++i)
#define FF(i, r, l) for (int i = (r); i >= (l); --i)
#define MAX(a, b) ((a) = max((a), (b)))
#define MIN(a, b) ((a) = min((a), (b)))
#define SZ(a) ((int)((a).size()) - 1)

const int MOD = 1000000007;

LL stir1[50005][205], C[205][205];

signed main(void)
{
	// freopen(".in", "r", stdin);
	// freopen(".out", "w", stdout);
	ios::sync_with_stdio(0), cin.tie(nullptr);

	stir1[0][0] = 1;
	F(i, 1, 50000) F(j, 1, 200)
		stir1[i][j] = (stir1[i - 1][j - 1] + stir1[i - 1][j] * (i - 1)) % MOD;
	F(i, 0, 200) C[i][0] = 1;
	F(i, 1, 200) F(j, 1, 200) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD;

	int _; cin >> _;
	while (_--)
	{
		int n, a, b;
		cin >> n >> a >> b;
		if (a + b - 2 > n - 1) { cout << "0\n"; continue; }
		cout << stir1[n - 1][a + b - 2] * C[a + b - 2][a - 1] % MOD << '\n';
	}

	return 0;
}

联合省选2022 学术社区

一个学术社区内有 \(n\) 个人,有 \(m\) 条消息。一个消息有唯一一个发出者,它可能是学术消息,或者“某人楼上”或者“某人楼下”。每个人至少发出一条学术消息。现在要求重新排列消息,使得“某人楼上”,或“某人楼下”类消息中符合实际的消息数目最多。输出答案并构造方案。

数据范围:\(1\le T\le 100, 1\le n\le m\le 77777,\sum m\le 2.5\times10^5\)

特殊性质:A 没有楼上型消息;B 存在一种方案使得所有楼上楼下消息都是合法的;C 不存在两条消息,使得在只有它们两个构成的排列中,两者均符合实际。

观察题目:楼上楼下型的消息可以转化为一系列规则,形如:如果消息 \(a\) 在消息 \(b\) 前发出,那么产生贡献 \(1\)。于是你可以把它想像成一张有向图,其中边从应当在前面的信息指向应当在后面的信息。注意到这个图里是有重边和自环的,我们先处理掉。首先考虑重边:重边的来源是两条消息,互为楼上楼下(即特殊性质 C 保证不存在的消息对),这种对必定直接互相匹配然后扔掉。理由是如果存在一种方法它们被放在其它地方,并且没有另外的互相匹配占据位置,那么可以将它们后部连接的其它消息和他们本身一起移动进行调整,分类讨论两种情况,可以论证这时直接匹配总是不劣。然后考虑一些自环,解决方法是直接别连。处理掉重边自环以后,问题就转化为了:找到简单图上的一系列点不交路径,使得总长度最大。

这个东西如果在 DAG 上,看起来很像最小路径覆盖。先考虑直接按照 DAG 最小路径覆盖做会怎样,也即拆点建立二分图跑最大匹配。我们会得到一些路径和一些环。环需要断开一条边才能真正合法,容易想直接断开得到答案。然而,也许存在这样一种情况,一条路径开头是 \(c\),而某个环上有 \(b\to a\) 两个相邻点,使得存在 \(b\to c\) 的边。换句话说,有另一种方案,不需要减少一条匹配边,而是可以把环拆开后拼接到一条路径中。这其实对应了原先的“最小路径覆盖”的不唯一性。那么,是否总是能找到这样的方案呢?在一般图上是不能的,但是我们应当猜测,这道题中能。为什么呢?因为出题人反复强调一点:任何人都会发学术消息。这条性质在先前的推导中从未用到,现在我们可以基于此尝试拼接所有的环。

考虑这个环对应到题目中,是一连串楼上还是一连串楼下,显然二者对称,考虑楼下型。找到任意一条边断开,然后寻找路径拼接,可以理解为找到一个信息,发送它的人是链尾的楼上对象。后一种信息如果是链头对应的人的那条学术信息,那么就非常合适,因为它自生没有需求。然而,这一个学术信息可能已经有人想要了,也就是有某个信息形如这个类型楼下。对于这种情况,易得我们直接把原先的那个信息从这个学术信息上断开,接到当前环的链头,然后把环拆开的链尾接到学术信息上就可以解决。

接下来只要跑一个二分图匹配。注意到边数很多,这可以通过建立一些人对应的点来解决,网络还是单位网络,Dinic可以做到 \(\mathrm O(m\sqrt{m})\)

代码已咕。

Day 6 3/18

晚上开始发烧。

联合省选2022 卡牌

给定 \(n\) 张卡牌,每张卡牌上写有一个正整数 \(s_i\);给出 \(m\) 次询问,每次询问给出 \(c_i\) 个质数 \(p_{i, j}\),求有多少种 \(s\) 的子集,使得其乘积可以被给出的所有质数整除。输出时对 \(998244353\) 取模。

数据范围:\(1\le n\le 10^6, 1\le m\le 1500, 1\le \sum c_i\le 18000, 1\le p_{i, j}, s_i\le 2000\)

值域很小,只涉及素因子,第一反应就是根号分治。\(2000\) 以内的素数不会超过 \(350\) 个,其中小于等于 \(\sqrt{2000}\) 的共有 \(13\) 个。如果针对这 \(13\) 个素数做指数级枚举,那么剩下的素数满足任何一个 \(s\) 中的元素只会被其中一个整除。考虑此时如何计算方案(不考虑前 \(13\) 个素数),答案为 \(2^n\cdot\prod\limits_{p > \sqrt{2000}}(1 - 2^{-tot_{p}})\)。预先统计一下 \(tot\) 即可做到 \(\mathrm O(\text{素数个数})\)

接下来考虑一下前面的 \(13\) 个,考虑容斥,钦定其中某些不满足,其它无所谓,这样相当于钦定某些数不能选。于是相当于把上面式子中的 \(n\)\(tot\) 换成一个子集的统计结果。显然这可以通过 \(\mathrm O(\sum \log s_i)\) 的预处理将每个数放入其对应的大素数和对应的小素数集合的位置中,然后对于所有大素数,进行高维前缀和即可得到所有的 \(tot'_p\)\(n'\)。时间复杂度 \(\mathrm O(2^{13}(\sigma c_i))\),随便过。

这题比较套路。我卡住的点是2000的平方根以内的素数不会太多。给我的启示是:一定要牢记自己脑子不好,该算和写的东西不要毛估估。

// Author: kyEEcccccc

#include <bits/stdc++.h>

using namespace std;

using LL = long long;
using ULL = unsigned long long;

#define F(i, l, r) for (int i = (l); i <= (r); ++i)
#define FF(i, r, l) for (int i = (r); i >= (l); --i)
#define MAX(a, b) ((a) = max(a, b))
#define MIN(a, b) ((a) = min(a, b))
#define SZ(a) ((int)((a).size()) - 1)

const int N = 1000005, M = 1505, MOD = 998244353;
const int PR[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41}, TP = 13;

int n, m, a[N];
vector<int> pr; int mnp[2005];
map<int, int> rk, rrk;
int tot[300][1 << 13], tot0[1 << 13];
LL pw2[N];

signed main(void)
{
    freopen("card.in", "r", stdin);
    freopen("card.out", "w", stdout);
    ios::sync_with_stdio(0), cin.tie(nullptr);

    cin >> n;
    F(i, 1, n) cin >> a[i];

    F(i, 2, 2000)
    {
        if (mnp[i] == 0) mnp[i] = i, pr.push_back(i);
        for (auto p : pr)
        {
            if (p * i > 2000) break;
            mnp[i * p] = p;
            if (p == mnp[i]) break;
        }
    }
    vector<int> tmp;
    for (auto pp : pr) if (pp > 41) tmp.push_back(pp), rk[pp] = tmp.size() - 1;
    pr.swap(tmp);
    // cerr << pr.size() << '\n';

    F(i, 1, n)
    {
        int w = 0;
        F(j, 0, TP - 1) while (a[i] % PR[j] == 0) a[i] /= PR[j], w |= 1 << j;
        if (a[i] != 1)
        {
            int bel = rk[a[i]];
            tot[bel][(1 << TP) - w - 1] += 1;
        }
        tot0[(1 << TP) - w - 1] += 1;
    }

    F(k, 0, TP - 1)
        for (int j = 0; j < (1 << TP); j += 1 << k + 1)
            F(t, 0, (1 << k) - 1)
                tot0[j + t] += tot0[j + t + (1 << k)];
    F(i, 0, SZ(pr))
        F(k, 0, TP - 1)
            for (int j = 0; j < (1 << TP); j += 1 << k + 1)
                F(t, 0, (1 << k) - 1)
                    tot[i][j + t] += tot[i][j + t + (1 << k)];

    cin >> m;
    vector<int> p, t;
    F(i, 0, TP - 1) rrk[PR[i]] = i;
    pw2[0] = 1;
    F(i, 1, n) pw2[i] = pw2[i - 1] * 2 % MOD;
    F(_, 1, m)
    {
        int c; cin >> c;
        p.clear(); t.clear();
        F(i, 0, c - 1)
        {
            int x; cin >> x;
            if (x <= 41) t.push_back(rrk[x]); else p.push_back(rk[x]);
        }

        LL ans = 0;
        F(ww, 0, (1 << t.size()) - 1)
        {
            int w = 0;
            F(i, 0, SZ(t)) if (ww >> i & 1) w |= 1 << t[i];

            LL res = 1; int tt = 0;
            F(i, 0, SZ(p)) res = res * (pw2[tot[p[i]][w]] - 1) % MOD, tt += tot[p[i]][w];
            res = res * pw2[tot0[w] - tt] % MOD;

            if (__builtin_parity(ww)) ans -= res; else ans += res;
        }

        cout << (ans % MOD + MOD) % MOD << '\n';
    }
    
    return 0;
}

Day 7 3/18

上午还在发烧,在宿舍呆了一天。

Day 8 3/19

在宿舍呆了一天

Day 9 3/20

今天不能再不做题了,但是还是头疼。

联合省选2022 序列变换

括号序题目,可以考虑括号树。考虑第一种操作,本质是将后一个括号的子树全部塞到前一个括号下面,然后把它本身也接到前一个括号下面。第二种操作则告诉我们子树可以交换,也就是说子树的顺序不重要。最终目的是把括号树变成一条链。

分三种情况讨论。如果 \(x = y = 0\),哈哈哈。如果 \(x = 0, y = 1\)\(x = y = 1\),那么显然每一层都保留最大的,然后先往最小的里面合并,再把最小的往最大里面合并即可。

如果 \(x = 1, y = 0\),则每一层一定都形如将其它所有的都合并入最小的那个,再将最小的与最后要保留的合并,代价为 \(w_s + w_{min}(tot - 2)\)(其中 \(w_s\) 为保留的那个)。考虑最终所有数一定都会作为那个 \(w_s\) 出现恰好一次(除了最后一层的那个节点)。那么每一层我就只需要做两件事情,第一件是保证下面层的最小值最小,第二件是尽可能让最大值被传到最后一层不用算。如果当前层的大小(指前面所有层下放完毕后)大于 \(2\),那么不用决策,随便选一个不是最小最大的数下放即可。如果当前层的大小是 \(2\),所有这样的层构成一个前缀,并且最后会留下某个数,其它所有数都会作为 \(w_s\) 被计算一次,那么容易得出,我保留的这个数要么是这些层中的最小数,要么是最大数,直接两种取个 \(\min\) 即可。

对题目中的操作建立一个好一点的直觉,不要先入为主地死磕一个转化,有时候需要忘掉自己先前的假设,去看一看题目里的操作实际上是什么,有什么条件,哪些条件比较松等等。总之就是建立好的直觉。

// Author: kyEEcccccc

#include <bits/stdc++.h>

using namespace std;

using LL = long long;
using ULL = unsigned long long;

#define F(i, l, r) for (int i = (l); i <= (r); ++i)
#define FF(i, r, l) for (int i = (r); i >= (l); --i)
#define MAX(a, b) ((a) = max(a, b))
#define MIN(a, b) ((a) = min(a, b))
#define SZ(a) ((int)((a).size()) - 1)

const int N = 400005;

int n, A, B, w[N];
string str;
struct Node { LL w; int par; vector<int> sub; } btr[N];
int tot_btr;
vector<LL> layer[N];

void dfs(int u, int k)
{
	layer[k].push_back(btr[u].w);
	for (int v : btr[u].sub) dfs(v, k + 1);
}

LL work(int st)
{
	LL ans = 0;
	multiset<LL> s;
	F(t, st, n - 1)
	{
		for (LL w : layer[t]) s.insert(w);
		ans += (s.size() - 2) * *s.begin() + *prev(s.end(), 2);
		s.erase(prev(s.end(), 2));
	}
	assert(s.size() == 1);
	return ans;
}

signed main(void)
{
	// freopen("bracket.in", "r", stdin);
	// freopen("bracket.out", "w", stdout);
	ios::sync_with_stdio(0), cin.tie(nullptr);

	cin >> n >> A >> B >> str;
	F(i, 1, n) cin >> w[i];

	int cur_node = 1, tot_left = 0;
	btr[1].w = 0;
	tot_btr = 1;
	F(i, 0, n * 2 - 1)
	{
		if (str[i] == '(')
		{
			btr[cur_node].sub.push_back(++tot_btr);
			btr[tot_btr].par = cur_node;
			btr[tot_btr].w = w[++tot_left];
			cur_node = tot_btr;
		}
		else cur_node = btr[cur_node].par;
	}

	dfs(1, 0);

	if (A == 0 && B == 0) cout << "0\n";
	else if (A == 1 && B == 1)
	{
		LL ans = 0, sum = 0;
		multiset<LL> s;
		F(t, 1, n - 1)
		{
			for (LL w : layer[t]) sum += w, s.insert(w);
			LL mn = *s.begin(), mx = *prev(s.end());
			ans += sum + mn * ((int)s.size() - 2);
			sum -= mx;
			s.erase(prev(s.end()));
		}
		cout << ans << '\n';
	}
	else if (A == 0 && B == 1)
	{
		LL ans = 0, sum = 0;
		multiset<LL> s;
		F(t, 1, n - 1)
		{
			for (LL w : layer[t]) sum += w, s.insert(w);
			LL mx = *prev(s.end());
			ans += sum - mx;
			sum -= mx;
			s.erase(prev(s.end()));
		}
		cout << ans << '\n';
	}
	else
	{
		LL sum = 0, mn = numeric_limits<LL>::max(), mx = numeric_limits<LL>::min();
		int tot = 0;
		bool worked = 0;
		F(t, 1, n - 1)
		{
			tot += layer[t].size();
			if (tot == 1) { --tot; continue; }
			if (tot > 2)
			{
				if (mx == numeric_limits<LL>::min()) cout << work(t) << '\n';
				else
				{
					layer[t].push_back(mn);
					LL ans = sum - mn + work(t);
					layer[t].pop_back();
					layer[t].push_back(mx);
					MIN(ans, sum - mx + work(t));
					cout << ans << '\n';
				}
				worked = 1;
				break;
			}
			for (LL w : layer[t]) sum += w, MAX(mx, w), MIN(mn, w);
			--tot;
		}
		if (!worked)
		{
			if (mn == numeric_limits<LL>::max()) cout << "0\n";
			else cout << sum - mx << '\n';
		}
	}
	
	return 0;
}

联合省选2021B 取模

我,真是个大笨蛋啊。

首先考虑枚举模数,其它数全部取个模,然后答案要么是最大的两个数相加减掉模数,要不然就枚举小的那个数,双指针枚举最大的第二个数使得两数之和小于模数,这样是 \(\Theta(n)\) 的,总共就是 \(\Theta(n^2\log n)\) 的(\(\log\) 来自排序)。

接下来考虑一个愚蠢无比的优化:从大到小枚举模数,如果答案比当前模数大,就直接结束。这样,你变成 \(\mathrm O(n\log n\log V)\) 了。然后没了。

啊?这是为啥?我不理解啊?别急。考虑 枚举模数时 先将 \(a\) 排序去重,设 \(ans < a_m < a_{m + 1} < \dots < a_n\),对于所有 \(i\), 有 \(ans \ge a_{i - 1} + a_{i - 2} - a_i\),也就是 \(a_i - ans \ge a_{i - 1} - ans + a_{i - 2} - ans\)。又有 \(a_m > 0 \wedge a_{m + 1} > 0\) 成立。所以数列 \(f_i = a_i - ans\) 的增长速度不慢于斐波那契数列。又由于 \(ans > 0\) 所以 \(f\) 的递推长度不会超过 \(\log V\) 级别,也就是上面我们只枚举了很少的模数。

一个暴力剪枝看起来很难卡的时候,它有可能就是对的。证明比猜测难,顺推比证明难,而你是个大笨蛋。

// Author: kyEEcccccc

#include <bits/stdc++.h>

using namespace std;

using LL = long long;
using ULL = unsigned long long;

#define F(i, l, r) for (int i = (l); i <= (r); ++i)
#define FF(i, r, l) for (int i = (r); i >= (l); --i)
#define MAX(a, b) ((a) = max(a, b))
#define MIN(a, b) ((a) = min(a, b))
#define SZ(a) ((int)((a).size()) - 1)

const int N = 200005;

int n, a[N], b[N];

signed main(void)
{
	// freopen(".in", "r", stdin);
	// freopen(".out", "w", stdout);
	ios::sync_with_stdio(0), cin.tie(nullptr);

	cin >> n;
	F(i, 1, n) cin >> a[i];
	sort(a + 1, a + n + 1);
	int ans = INT_MIN;
	FF(k, n, 1)
	{
		if (k != n && a[k] == a[k + 1]) continue;
		if (a[k] <= ans) break;
		F(i, 1, n) b[i] = a[i] % a[k];
		sort(b + 1, b + n + 1);
		MAX(ans, b[n] + b[n - 1] - a[k]);
		int p = n;
		F(i, 2, n)
		{
			while (p > 1 && b[i] + b[p] >= a[k]) --p;
			if (p == 1) break;
			MAX(ans, b[i] + b[p]);
		}
	}
	cout << ans << '\n';
	
	return 0;
}

Day 10-12 3/21-23

先补一下一些先前的代码,以及一些赛题。

posted @ 2023-03-13 21:53  kyEEcccccc  阅读(92)  评论(1编辑  收藏  举报