BITACM 2025 暑期集训 B组 题解

Solutions

[2025-07-15] DP基础

[BITACM SC 2025 Tryout H.] 判断正误

一共\(q\)组数据。每组数据给定字符串\(s\),每次操作可以删除其中一个长度至少为\(2\)的回文子串,剩余部分按原顺序拼接。判断能否在若干次操作后删空整个字符串。

保证\(s\)仅包含小写字母,\(1\leq|s|\leq300,\ q\leq10,\ \sum|s|\leq500\)

\(f_{i,j}\)表示\(s[i,i+j)\)能删空。

递推边界:

\[f_{i,-1}=f_{i,0}=1\ (i\in\N,\ i<n) \]

状态转移方程:

\[\begin{align} f_{i,j}= &\left(\bigvee_{k=1}^{j-1}(f_{i,k}\wedge f_{i+k,j-k})\right)\vee&&\mathtt{()()}\\ &((s_i=s_{i+j-1})\wedge f_{i+1,j-2})\vee&&\mathtt{x()x}\\ &\left((s_i=s_{i+j-1})\wedge\bigvee_{k=1}^{j-1}(f_{i+1,k}\wedge f_{i+k+1,j-k-1})\right)&&\mathtt{x()y()x} \end{align} \]

代码:

inline void solve() {
	size_t n;
	std::string s;
	std::cin >> n >> s;
	auto dp = std::vector<std::vector<bool>>(n);
	for (size_t i = 0; i < n; ++i) {
		dp[i].resize(n - i + 1);
	}
	for (size_t i = 0; i < n; ++i) {
		dp[i][0] = true;
	}
	for (size_t i = 2; i <= n; ++i) {
		for (size_t j = 0; j + i <= n; ++j) {
			if (s[j] == s[j + i - 1]) {
				if (dp[j + 1][i - 2]) {
					dp[j][i] = true;
					continue;
				}
				for (size_t k = 0; k < i - 2; ++k) {
					if (dp[j + 1][k] && dp[j + k + 2][i - k - 3]) {
						dp[j][i] = true;
						break;
					}
				}
				if (dp[j][i]) {
					continue;
				}
			}
			for (size_t k = 1; k < i; ++k) {
				if (dp[j][k] && dp[j + k][i - k]) {
					dp[j][i] = true;
					break;
				}
			}
		}
	}
	std::cout << (dp[0][n] ? "YES\n" : "NO\n");
}

[2025-07-16] 树状数组和线段树

[Luogu P3870] [TJOI 2009] 开关

一共\(n\)盏灯。执行\(q\)次操作,每次操作指定操作类型和一个区间。一共有两种操作类型:①翻转区间内所有灯的状态。②查询区间内点亮的灯的数量。

保证\(n,q\in\N,\ 2\leq n\leq10^5,\ 1\leq q\leq10^5\)

用线段树处理。线段树顶点有两个值:\(sum\)维护区间内点亮的灯的数量,懒标记\(lzy\)维护其子区间是否需要翻转状态。下传时,如果当前节点有\(lzy\),则翻转子节点,即计算子节点的\(sum\gets end-beg-sum\),并翻转子节点的\(lzy\)

struct Node {
	size_t sum;
	bool lzy;
};

size_t n;
std::vector<Node> nodes;

inline size_t leftChild(size_t rt) { return ((rt << 1) | 1); }
inline size_t rightChild(size_t rt) { return ((rt + 1) << 1); }

inline void buildTree(size_t n) {
	nodes.resize(n << 2);
}

inline void pushUp(size_t rt) {
	nodes[rt].sum = nodes[leftChild(rt)].sum + nodes[rightChild(rt)].sum;
}
inline void pushDown(size_t rt, size_t nd_beg, size_t nd_end) {
	if (!nodes[rt].lzy) {
		return;
	}
	nodes[rt].lzy = false;
	size_t nd_mid = nd_beg + ((nd_end - nd_beg) >> 1);
	size_t lch = leftChild(rt), rch = rightChild(rt);
	nodes[lch].lzy ^= 1;
	nodes[lch].sum = (nd_mid - nd_beg) - nodes[lch].sum;
	nodes[rch].lzy ^= 1;
	nodes[rch].sum = (nd_end - nd_mid) - nodes[rch].sum;
}

void flip(size_t index,
		  size_t nd_beg, size_t nd_end,
		  size_t beg, size_t end) {
	if (beg <= nd_beg && nd_end <= end) {
		nodes[index].sum = (nd_end - nd_beg) - nodes[index].sum;
		nodes[index].lzy ^= 1;
		return;
	}
	pushDown(index, nd_beg, nd_end);
	size_t nd_mid = nd_beg + ((nd_end - nd_beg) >> 1);
	if (beg < nd_mid) {
		flip(leftChild(index), nd_beg, nd_mid, beg, end);
	}
	if (nd_mid < end) {
		flip(rightChild(index), nd_mid, nd_end, beg, end);
	}
	pushUp(index);
}
inline void flip(size_t pos, size_t len) { flip(0, 0, n, pos, pos + len); }
size_t query(size_t index,
			 size_t nd_beg, size_t nd_end,
			 size_t beg, size_t end) {
	if (beg <= nd_beg && nd_end <= end) {
		return nodes[index].sum;
	}
	pushDown(index, nd_beg, nd_end);
	size_t nd_mid = nd_beg + ((nd_end - nd_beg) >> 1);
	size_t res = 0;
	if (beg < nd_mid) {
		res += query(leftChild(index), nd_beg, nd_mid, beg, end);
	}
	if (nd_mid < end) {
		res += query(rightChild(index), nd_mid, nd_end, beg, end);
	}
	return res;
}
inline size_t query(size_t pos, size_t len) {
	return query(0, 0, n, pos, pos + len);
}

分块在这道题的数据范围下也可以做。

[Luogu P1972] [SDOI 2009] HH的项链

给定数组\(\vec a\in(\N\cap[1,10^6])^n\ (n\in\N)\),进行\(q\)次查询,每次查询\(\vec a\)在区间\([l,r]\)的元素种类数。

保证\(n,q,l,r\in\N\cap[1,10^6],l\leq r\leq n\)

此题没有修改,全是查询,可以考虑将查询离线处理。

由于所求的是区间里的元素种类数,可以考虑每个元素有没有贡献新的种类。出于子问题思想,如果仅考虑前\(r\)个元素,计算出每个位置的贡献(记为\(\vec c\in\{0,1\}^r\)),那么对于任何满足条件的\(l\),所求值就是\(\sum_{i=l}^rc_i\)。对于重复出现的元素,因为后出现的更容易被\([l,r]\)覆盖,因此统一把贡献值给后出现的元素。

综合以上,于是有这样的思路:将查询按照右端点升序排序。从左往右遍历\(\vec a\),如果是新的元素就直接将该处贡献记为\(1\),否则将之前位置的贡献移动到当前位置。如果刚刚考虑的位置是某个查询的右端点,则计算其答案(\(\sum_{i=l}^rc_i\))。贡献值仅涉及单点的加减,而查询涉及区间求和,故可以用树状数组维护\(\vec c\)

struct Query {
	size_t idx, beg, end;
	uint32_t ans;
};

size_t n, q;
std::vector<uint32_t> necklace;
std::vector<Query> queries;

inline void preprocess() {
	std::cin >> n;
	necklace.resize(n);
	for (auto &x : necklace) {
		std::cin >> x;
		--x;
	}
	std::cin >> q;
	queries.resize(q);
	for (size_t i = 0; i != q; ++i) {
		queries[i].idx = i;
		std::cin >> queries[i].beg >> queries[i].end;
		--queries[i].beg;
	}
	std::sort(queries.begin(), queries.end(),
			  [](const Query &lhs, const Query &rhs) {
				  return (lhs.end < rhs.end);
			  });
}

inline void solve() {
	auto last = std::vector<size_t>(
		*std::max_element(necklace.begin(), necklace.end()) + 1, size_t(-1));
	auto ft = FenwickTree<uint32_t>(n);
	size_t cur = 0;
	for (auto &[_, beg, end, ans] : queries) {
		for (size_t i = cur; i != end; ++i) {
			if (~last[necklace[i]]) {
				ft.modify(last[necklace[i]], -1);
			}
			last[necklace[i]] = i;
			ft.modify(i, 1);
		}
		cur = end;
		ans = ft.query(end) - ft.query(beg);
	}
	std::sort(queries.begin(), queries.end(),
			  [](const Query &lhs, const Query &rhs) {
				  return (lhs.idx < rhs.idx);
			  });
	for (auto &query : queries) {
		std::cout << query.ans << '\n';
	}
}

时间复杂度:\(O(n\log n+q\log q)\)

空间复杂度:\(O(n)\)

[CF 1042D] Petya and Array

给定\(\vec a\in (\Z\cap[-2^{31},2^{31}))^n\ (n\in\N)\)\(l\in\Z\cap[-2\times10^{14},2\times10^{14})\),求\(\sum_{i=0}^{n-1}\sum_{j=i+1}^{n}\left(\sum_{k=i}^{j-1}a_k<l\right)\)

保证\(n\leq2\times10^5\)

\[\sum_{k=i}^{j-1}a_k=\left(\sum_{k=0}^{j-1}-\sum_{k=0}^{i-1}\right)a_k \]

设前缀和\(\vec s=\left(\sum_{j=0}^{i-1}a_j\right)_{i=0}^{n}\)(注意必须留一个\(0\)),则原问题转化为求\((i,j)\in\N^2\)满足\(0\leq i<j<n\wedge s_j<s_i+t\)的个数。类比树状数组求逆序对,将前缀和数组离散化后利用权值树状数组计数即可。

inline void solve() {
	size_t n;
	int64_t lim;
	std::cin >> n >> lim;
	auto arr = std::vector<int64_t>(n + 1);
	for (size_t i = 1; i <= n; ++i) {
		std::cin >> arr[i];
	}
	for (size_t i = 2; i <= n; ++i) {
		arr[i] += arr[i - 1];
	}
	auto unq = arr;
	std::sort(unq.begin(), unq.end());
	unq.erase(std::unique(unq.begin(), unq.end()), unq.end());
	auto rks = std::vector<size_t>(arr.size());
	auto bound = std::vector<size_t>(arr.size());
	for (size_t i = 0; i != arr.size(); ++i) {
		rks[i] = std::lower_bound(unq.begin(), unq.end(), arr[i]) - unq.begin();
		bound[i] = std::lower_bound(unq.begin(), unq.end(), arr[i] + lim) -
				   unq.begin();
	}
	auto ft = FenwickTree<uint64_t>(unq.size());
	uint64_t res = 0;
	for (size_t i = n; ~i; --i) {
		res += ft.query(bound[i]);
		ft.modify(rks[i], 1);
	}
	std::cout << res << std::endl;
}

[2025-07-17] DFS、BFS、最短路和拓扑排序

[Luogu P3243] [HNOI 2015] 菜肴制作

神秘省选题,最难的地方是阅读理解。

题目描述

知名美食家小 A 被邀请至 ATM 大酒店,为其品评菜肴。ATM 酒店为小 A 准备了 \(n\) 道菜肴,酒店按照为菜肴预估的质量从高到低给予 \(1\)\(n\) 的顺序编号,预估质量最高的菜肴编号为 \(1\)

由于菜肴之间口味搭配的问题,某些菜肴必须在另一些菜肴之前制作,具体的,一共有 \(m\) 条形如 \(i\) 号菜肴必须先于 \(j\) 号菜肴制作的限制,我们将这样的限制简写为 \((i,j)\)

现在,酒店希望能求出一个最优的菜肴的制作顺序,使得小 A 能尽量先吃到质量高的菜肴

也就是说,

  1. 在满足所有限制的前提下,\(1\) 号菜肴尽量优先制作。

  2. 在满足所有限制,\(1\) 号菜肴尽量优先制作的前提下,\(2\) 号菜肴尽量优先制作。

  3. 在满足所有限制,\(1\) 号和 \(2\) 号菜肴尽量优先的前提下,\(3\) 号菜肴尽量优先制作。

  4. 在满足所有限制,\(1\) 号和 \(2\) 号和 \(3\) 号菜肴尽量优先的前提下,\(4\) 号菜肴尽量优先制作。

  5. 以此类推。

例 1:共 \(4\) 道菜肴,两条限制 \((3,1)\)\((4,1)\),那么制作顺序是 \(3,4,1,2\)

例 2:共 \(5\) 道菜肴,两条限制 \((5,2)\)\((4,3)\),那么制作顺序是 \(1,5,2,4,3\)

例 1 里,首先考虑 \(1\),因为有限制 \((3,1)\)\((4,1)\),所以只有制作完 \(3\)\(4\) 后才能制作 \(1\),而根据 3,\(3\) 号又应尽量比 \(4\) 号优先,所以当前可确定前三道菜的制作顺序是 \(3,4,1\);接下来考虑 \(2\),确定最终的制作顺序是 \(3,4,1,2\)

\(2\) 里,首先制作 \(1\) 是不违背限制的;接下来考虑 \(2\) 时有 \((5,2)\) 的限制,所以接下来先制作 \(5\) 再制作 \(2\);接下来考虑 \(3\) 时有 \((4,3)\) 的限制,所以接下来先制作 \(4\) 再制作 \(3\),从而最终的顺序是 \(1,5,2,4,3\)。现在你需要求出这个最优的菜肴制作顺序。无解输出 Impossible!(首字母大写,其余字母小写)

输入格式

第一行是一个正整数 \(t\),表示数据组数。接下来是 \(t\) 组数据。对于每组数据:第一行两个用空格分开的正整数 \(n\)\(m\),分别表示菜肴数目和制作顺序限制的条目数。接下来 \(m\) 行,每行两个正整数 \(x,y\),表示 \(x\) 号菜肴必须先于 \(y\) 号菜肴制作的限制。

输出格式

输出文件仅包含 \(t\) 行,每行 \(n\) 个整数,表示最优的菜肴制作顺序,或者 Impossible! 表示无解。

输入输出样例

输入 #1

3
5 4
5 4
5 3
4 2
3 2
3 3
1 2
2 3
3 1
5 2
5 2
4 3

输出 #1

1 5 3 4 2 
Impossible! 
1 5 2 4 3

说明/提示

【样例解释】

第二组数据同时要求菜肴 \(1\) 先于菜肴 \(2\) 制作,菜肴 \(2\) 先于菜肴 \(3\) 制作,菜肴 \(3\) 先于菜肴 \(1\) 制作,而这是无论如何也不可能满足的,从而导致无解。

【数据范围】

\(100\%\) 的数据满足 \(n,m\le 10^5\)\(1\le t\le 3\)

\(m\) 条限制中可能存在完全相同的限制。

最难理解的就是“尽量先吃质量高的菜”。它的意思是说,要在所有拓扑序中,按照如下优先级规则选取最优的顺序:

  1. \(1\)个菜越早上越好。

  2. \(2\)个菜越早上越好。

  3. \(3\)个菜越早上越好。

值得注意的是,这并不是字典序。例如,观察样例第三组数据,1 5 2 4 31 4 3 5 2都符合拓扑序,而字典序较小的1 4 3 5 2实际上将本可以第三个就端上来的菜肴\(2\)放在了最后一个。

为了实现题目所要求的顺序,我们可以为了一个远在一个拓扑块末端的菜肴\(2\)大费周章地先做一堆别的菜,然后再考虑下一个菜。在做那些“别的菜”时,也需要优先完成其中末尾小的拓扑块。这样考虑的话,那末,我们需要做的是反连原图,倒过来考虑。不过这样的话,产生的拓扑序也是反的,优先选取的应为队列中编号大的菜(这样编号小的菜就会早出现在原结果中),且需要倒序输出结果。

inline void solve() {
	size_t n, m;
	std::cin >> n >> m;
	auto adj = std::vector<std::vector<size_t>>(n);
	auto indeg = std::vector<size_t>(n);
	for (size_t i = 0; i != m; ++i) {
		size_t u, v;
		std::cin >> u >> v;
		adj[v - 1].emplace_back(--u);
		++indeg[u];
	}
	std::priority_queue<size_t> q;
	for (size_t i = 0; i != n; ++i) {
		if (!indeg[i]) {
			q.emplace(i);
		}
	}
	std::vector<size_t> res;
	while (q.size()) {
		auto from = q.top();
		q.pop();
		res.emplace_back(from);
		for (auto to : adj[from]) {
			if (!(--indeg[to])) {
				q.emplace(to);
			}
		}
	}
	if (res.size() != n) {
		std::cout << "Impossible!\n";
	} else {
		for (size_t i = res.size() - 1; ~i; --i) {
			std::cout << (res[i] + 1) << ' ';
		}
		std::cout << '\n';
	}
}

时间复杂度:\(O(n\log n)\)

空间复杂度:\(O(n)\)

[Luogu P1993] 小K的农场

有某个未知的\(\vec a\in(\N^*)^n\ (n\in\N^*)\),现给出\(m\in\N^*\)条约束,每条约束是以下两种类型中的一种:给定\(u,v\in\N\cap[0,n)\),①给定\(w\in\Z\)\(a_v-a_u\leq w\);②\(a_u=a_v\)。判断这些约束之间是否有矛盾。

保证\(n,m,|w|\leq5\times10^3\)

差分约束模板题。值得注意的是,原图可能不连通。在此题中一个较简便的方法是建立超级源点,向其他各点连接一条权值为\(0\)的边,因为此题仅需验证有无负环,而无需计算最终的大小关系。

inline void solve() {
	size_t n, m;
	std::cin >> n >> m;
	auto adj = std::vector<std::vector<std::pair<size_t, int32_t>>>(n + 1);
	for (size_t i = 0; i != m; ++i) {
		int op;
		size_t u, v;
		int32_t w;
		std::cin >> op >> v >> u;
		--u, --v;
		if (op == 3) {
			adj[u].emplace_back(v, 0);
			adj[v].emplace_back(u, 0);
		} else {
			std::cin >> w;
			if (op == 1) {
				std::swap(u, v);
				w = -w;
			}
			adj[u].emplace_back(v, w);
		}
	}
	adj[n].reserve(n);
	for (size_t i = 0; i != n; ++i) {
		adj[n].emplace_back(i, 0);
	}
	std::cout << (spfa(adj, n).empty() ? "No\n" : "Yes\n");
}

时间复杂度:最坏\(O(nm)\)

空间复杂度:\(O(n)\)

[2025-07-19] 单调栈和单调队列

[Luogu P1725] 琪露诺

给定\(n\in\N,\ \vec a\in\Z^{n+1}\)。人初始时在\(0\)位置,每次可以向右跳\([beg, end)\)步。跳到\(i\)处,总分就加上\(a_i\)。跳到\(i>n\)处时即视作到达终点。求总分最大值。

保证\(n\leq2\times10^5,\ (\forall i\in\N,\ i\leq n)(|a_i|\leq2\times10^3),\ 1\leq beg<end\leq n+1\)

扩展\(\vec a\)\(\Z^{n+1+end}\):令\(\vec a\gets (\vec a, \vec 0_{end})\)

DP是很容易想到的:设\(f_i\)为跳到\(i\)位置后的最大得分,易知

\[f_i=\max_{j=beg}^{\min\{i,end-1\}}f_{i-j}+a_i\ (i\in\N,\ (i=0\vee beg\leq i<n+1+end)) \]

值得注意的是,在这里\(f_i\ (i\in\N\cap(0,beg))\)是未定义的。为了允许区间\((beg,beg+end)\)的状态转移,将其定义为\(-\infty\)

朴素做法的时间复杂度为\(O(n\cdot(end - beg))\),在该题数据范围下是不能接受的。状态转移方程中唯一可以优化的地方就是那一坨\(\max\)了。使用单调队列可以在遍历过程中\(O(1)\)获得该的最大值,将总时间复杂度降到\(O(n)\)

inline void solve() {
	auto dp = std::vector<int32_t>(n + end + 1, INT32_MIN);
	auto cmp = [&](size_t lhs, size_t rhs) { return (dp[lhs] > dp[rhs]); };
	auto dq = MonoDeque<size_t, decltype(cmp)>(cmp);
	dp[0] = 0;
	for (size_t i = beg; i < n + end; ++i) {
		dq.emplaceBack(i - beg);
		if (dq.front() + end <= i) {
			dq.popFront();
		}
		maxEq(dp[i], dp[dq.front()] + arr[i]);
	}
	std::cout << *std::max_element(dp.begin() + n + 1, dp.end()) << '\n';
}

时间复杂度:\(O(n)\)

空间复杂度:\(O(n)\)

[Luogu P4147] 玉蟾宫

给定一个矩阵\(grid\in\{\mathtt{'R'},\mathtt{'F'}\}^{n\times m}\ (n,m\in\N^*)\),求\(\max_{x_0,x_1,y_0,y_1\in\N,\ x_0\leq x_1<n,\ y_0<y_1\leq m,\ \forall(x,y)\in\N^2\cap[x_0,x_1)\times[y_0,y_1)\ (grid_{x,y}=\mathtt{'F'})}(x_1-x_0)(y_1-y_0)\)

保证\(n,m\in[1,1000]\)

逐行读入,记录同一列的连续\(\mathtt{'F'}\)高度。这样,原问题就转化为了经典的最大矩形问题。

size_t n, m;
std::vector<std::vector<char>> grid;

inline uint32_t maxRect(const std::vector<uint32_t> &arr) {
	std::stack<size_t, std::vector<size_t>> stk;
	auto begs = std::vector<size_t>(arr.size()),
		 ends = std::vector<size_t>(arr.size());
	for (size_t i = 0; i != arr.size(); ++i) {
		while (stk.size() && arr[stk.top()] > arr[i]) {
			ends[stk.top()] = i;
			stk.pop();
		}
		stk.emplace(i);
	}
	while (stk.size()) {
		ends[stk.top()] = arr.size();
		stk.pop();
	}
	for (size_t i = arr.size() - 1; ~i; --i) {
		while (stk.size() && arr[stk.top()] > arr[i]) {
			begs[stk.top()] = i + 1;
			stk.pop();
		}
		stk.emplace(i);
	}
	while (stk.size()) {
		begs[stk.top()] = 0;
		stk.pop();
	}
	uint32_t res = 0;
	for (size_t i = 0; i != arr.size(); ++i) {
		maxEq<uint32_t>(res, (ends[i] - begs[i]) * arr[i]);
	}
	return res;
}

inline void solve() {
	auto heights = std::vector<uint32_t>(m);
	uint32_t res = 0;
	for (auto &row : grid) {
		for (size_t i = 0; i != m; ++i) {
			if (row[i] == 'F') {
				++heights[i];
			} else {
				heights[i] = 0;
			}
		}
		maxEq(res, maxRect(heights));
	}
	std::cout << (res * 3) << '\n';
}

时间复杂度:\(O(nm)\)

空间复杂度:\(O(nm)\)。一边读入一边计算可以将空间复杂度优化到\(O(m)\)

[2025-07-20] 贪心、二分和双指针

[CF 2023A] Concatenation of Arrays

给定\(n\in\N\)个二元组,将它们排序使得拼接后逆序对数最少。只需求出一种排序方案即可。

考虑相邻两个二元组\((a_0,a_1)\)\((b_0,b_1)\)。由于二元组内顺序无法改变,故忽略二元组内的逆序对,不妨设\(a_0\leq a_1,\ b_0\leq b_1\)

现在考虑它们的大小关系。

  1. 最好的情况:\(a_1\leq b_0\)。顺序。
  2. \(a_0\leq b_0<a_1<b_1\):顺序(当前逆序对数为\(1\),倒序则逆序对数为\(3\))。
  3. \(a_0\leq b_0<a_1=b_1\):顺序(倒序则逆序对数可能增加)。
  4. \(a_0\leq b_0\leq b_1<a_1\):无所谓(无论顺序还是逆序,所得逆序对数都为\(2\))。

结论就是,应当这样排序这些二元组,使得对于从左往右的两项\((a_0,a_1),(b_0,b_1)\),满足\(\max\vec a<\max\vec b\vee(\max\vec a=\max\vec b\wedge\min\vec a<\min\vec b)\)。容易证明这是个偏序关系,所以是正确的。

inline void solve() {
	size_t n;
	std::cin >> n;
	auto arr = std::vector<std::pair<uint32_t, uint32_t>>(n);
	for (auto &[x, y] : arr) {
		std::cin >> x >> y;
	}
	std::sort(arr.begin(), arr.end(),
			  [](const auto &lhs, const auto &rhs) {
				  auto lm = std::max(lhs.first, lhs.second),
					   rm = std::max(rhs.first, rhs.second);
				  return ((lm == rm)
							  ? (std::min(lhs.first, lhs.second) <
								 std::min(rhs.first, rhs.second))
							  : (lm < rm));
			  });
	for (auto &[x, y] : arr) {
		std::cout << x << ' ' << y << ' ';
	}
	std::cout << std::endl;
}

[2025-07-21] BITACM SC 2025 Rating #1

按照比赛时做题顺序排列。

[K] 小黑子

看了几道题都感觉不好做,一看排名有个人试了一次K没过,一看简单到爆,秒了。

检查给定字符串中是否包含子串"jntm",忽略大小写。

inline void solve() {
	constexpr char JNTM[] = "jntm";
	std::string s;
	std::cin >> s;
	for (size_t i = 0; i + 4 <= s.size(); ++i) {
		bool ok = true;
		for (size_t j = 0; j != 4; ++j) {
			if (std::tolower(s[i + j]) != JNTM[j]) {
				ok = false;
				break;
			}
		}
		if (ok) {
			std::cout << "yes\n";
			return;
		}
	}
	std::cout << "no\n";
}

[D] 构造序列

写完K看见有人过了D,一看就求个差分数组,秒了。

给定\(\vec s\in\Z^n\ (n\in\N^*)\),构造\(\vec a\in\Z^n\)满足\((\forall i\in\N,\ i<n)\ (\sum_{j=0}^{i}a_j=s_i)\)

才发现甚至给的符号都不演了。

inline void solve() {
    size_t n;
    std::cin >> n;
    auto arr = std::vector<int64_t>(n);
    for (auto &x : arr) {
        std::cin >> x;
    }
    auto diff = std::vector<int64_t>(n);
    diff[0] = arr[0];
    for (size_t i = 1; i < n; ++i) {
        diff[i] = arr[i] - arr[i - 1];
    }
    for (auto &x : diff) {
        std::cout << x << ' ';
    }
    std::cout << std::endl;
}

[G] 星露谷物语

我如果没记错的话,原题是“填平土地”。

平地上有连续并排的\(n\in\N^*\)个坑,深度分别为\(\vec a\in\N^n\)。每次填坑可以选定一个区间\([b,e)\)的土地使其高度增加\(1\)。求最少需要多少次填坑才能恰好把地面填平。

经典单调栈题目。补充定义\(a_{-1}=a_n=0\)。设对于第\(i\)个坑,它左边第一个小于\(a_i\)的数在\(l_i\)处,右边第一个小于等于\(a_i\)的数在\(r_i\)处;更严谨地,设\(l_i=\max\{j\in\Z:\ -1\leq j<i\wedge a_j<a_i\},\ r_i=\min\{j\in\Z:\ i<j\leq n\wedge a_j\leq a_i\}\)。那末\(i\)的贡献(即“凸出来”不能与旁边的点一起填掉的部分)是\(a_i-\max\{a_{l_i},a_{r_i}\}\)。之所以一个取小于等于,一个取小于,是因为:如果都取小于等于,那末连续一段相等的值就会没有计入;如果都取小于,则又会对连续相等的值重复计算。

inline void solve() {
    size_t n;
    std::cin >> n;
    auto arr = std::vector<uint32_t>(n);
    for (auto &x : arr) {
        std::cin >> x;
    }
    std::stack<size_t> stk;
    auto left = std::vector<size_t>(n), right = std::vector<size_t>(n);
    for (size_t i = 0; i != n; ++i) {
        while (stk.size() && arr[stk.top()] >= arr[i]) {
            right[stk.top()] = i;
            stk.pop();
        }
        stk.push(i);
    }
    while (stk.size()) {
        right[stk.top()] = n;
        stk.pop();
    }
    for (size_t i = n - 1; ~i; --i) {
        while (stk.size() && arr[stk.top()] > arr[i]) {
            left[stk.top()] = i;
            stk.pop();
        }
        stk.push(i);
    }
    while (stk.size()) {
        left[stk.top()] = -1;
        stk.pop();
    }
    uint32_t res = 0;
    for (size_t i = 0; i != n; ++i) {
        uint32_t l = ((left[i] == size_t(-1)) ? 0 : arr[left[i]]);
        uint32_t r = ((right[i] == n) ? 0 : arr[right[i]]);
        res += arr[i] - std::max(l, r);
    }
    std::cout << res << std::endl;
}

时间复杂度:\(O(n)\)

空间复杂度:\(O(n)\)

[I] 相似的字符串

给定\(n\in\N^*\)个长为\(m\in\N^*\)的字符串,判断其是否存在一种排列,使得相邻两项有且仅有一个字符不同。

保证\(2\leq n\leq8,\ m\leq 5\)

数据范围这么小,直接暴力。但是数据实在是太水了,我写错了居然也过了。我判断的是相邻两项最多有一个字符不同。

inline bool isSim(const std::string &s, const std::string &t) {
    if (s.size() != t.size()) {
        return false;
    }
    size_t n = s.size();
    bool has_diff = false;
    for (size_t i = 0; i < n; ++i) {
        if (s[i] != t[i]) {
            if (has_diff) {
                return false;
            }
            has_diff = true;
        }
    }
    return true;
}
 
inline void solve() {
    size_t n, m;
    std::cin >> n >> m;
    auto strs = std::vector<std::string>(n);
    for (auto &s : strs) {
        std::cin >> s;
    }
    auto order = std::vector<size_t>(n);
    for (size_t i = 0; i < n; ++i) {
        order[i] = i;
    }
    auto perm = std::vector<std::string>(n);
    do {
        for (size_t i = 0; i < n; ++i) {
            perm[i] = strs[order[i]];
        }
        bool is_sim = true;
        for (size_t i = 0; i + 1 < n; ++i) {
            if (!isSim(perm[i], perm[i + 1])) {
                is_sim = false;
                break;
            }
        }
        if (is_sim) {
            std::cout << "Yes\n";
            return;
        }
    } while (std::next_permutation(order.begin(), order.end()));
    std::cout << "No\n";
}

时间复杂度:\(O(n!nm)\)

空间复杂度:\(O(nm)\)

[H] 相似的数字

给定\(\vec a\in(\N^*)^m,\ \vec b\in(\N^*)^n\ (m,n\in\N^*),\ d\in\N\),求\(\max_{i,j\in\N,\ i<m,\ j<n,\ |b_j-a_i|\leq d}(a_i+b_j)\)

\(\vec b\)排序。枚举\(i\),在\(\vec b\)中二分查找最大的\(j\)满足\(b_j\leq a_i+d\),如果有\(b_j\geq a_i-d\),则\(res\gets\max\{res,a_i+b_j\}\)

特别地,由于\(\vec a,\vec b\)等价,可以选择更短的一个作为\(\vec a\)

inline void solve() {
    size_t m, n;
    uint64_t d;
    std::cin >> m >> n >> d;
    auto a = std::vector<uint64_t>(m), b = std::vector<uint64_t>(n);
    for (auto &x : a) {
        std::cin >> x;
    }
    for (auto &x : b) {
        std::cin >> x;
    }
    if (m > n) {
        std::swap(m, n);
        std::swap(a, b);
    }
    std::sort(b.begin(), b.end());
    uint64_t max = 0;
    for (auto x : a) {
        auto it = std::upper_bound(b.begin(), b.end(), x + d);
        if (it == b.begin() || *(--it) + d < x) {
            continue;
        }
        maxEq(max, x + *it);
    }
    if (max) {
        std::cout << max << std::endl;
    } else {
        std::cout << "-1\n";
    }
}

时间复杂度:\(O((m+n)\log n)\)

空间复杂度:\(O(n)\)

[C] 序列查询

设有这样一个空序列\(\vec a\)。按顺序执行\(q\in\N^*\)个操作。每次操作是以下三种中的一种:

  1. 将一个元素\(x\in\N^*\)追加到\(\vec a\)末尾。
  2. 查询\(\vec a\)的第一个元素,并将其删除。
  3. \(\vec a\)升序排序。

保证\(q\leq2\times10^5\)

前两个操作就是“队列”的定义,而能一直保持升序的队列就是“优先队列”。由于排序后,取出元素的顺序是先取之前排好了顺序的部分,再按照加入顺序取后面加入但尚未排序的部分,于是我们可以维护一个升序优先队列\(pq\)和一个普通的队列\(q\)。每次添加元素时,先加到\(q\)队尾;取元素时,优先从\(pq\)取,若\(pq\)为空就从\(q\)取;排序时,只需将\(q\)中所有元素移动到\(pq\)即可。

std::priority_queue<int64_t, std::vector<int64_t>, std::greater<int64_t>> pq;
std::queue<int64_t> q;

inline void solve() {
	int op;
	std::cin >> op;
	if (op == 1) {
		int64_t x;
		std::cin >> x;
		q.push(x);
	} else if (op == 2) {
		if (pq.size()) {
			std::cout << pq.top() << '\n';
			pq.pop();
		} else {
			std::cout << q.front() << '\n';
			q.pop();
		}
	} else if (op == 3) {
		while (q.size()) {
			pq.push(q.front());
			q.pop();
		}
	}
}

时间复杂度:压入操作为\(O(1)\),弹出操作为\(O(\log|pq|)\),排序操作大约为\(O(|q|\log|pq|)\)。每个元素贡献的时间不超过\(\log q\),元素个数不超过\(q\),总的时间复杂度小于\(O(q\log q)\),其中\(q\)是操作次数。

空间复杂度:不超过\(O(q)\),其中\(q\)是操作次数。

[A] 后缀自动鸡

给定\(l,m,n\in\N^*\)和长为\(n\)的字符串\(s\)。每次操作可以选定\(s\)的一个长度不超过\(l\)的子串,使其按降序排序。求在不超过\(m\)次操作中能得到的字典序最大的字符串。

保证\(n\leq10^6,\ n\cdot n\leq m\leq10^18,\ l\leq n\)

傻逼诈骗题,\(n^2\)不写\(n^2\),非要写成\(n\cdot n\)

冒泡排序的交换次数是原序列的逆序对数,长为\(n\)的序列的逆序对数不超过\(\binom{n}{2}=\frac{1}{2}n(n-1)<\frac{1}{2}n^2<n^2\)。所以操作次数额度是远远够了的。只要\(l\geq2\),就一定可以通过交换相邻项,冒泡排序得到完全降序排序的字符串。\(l=1\)时直接返回原串即可,因为根本不能排序。

inline void solve() {
    size_t l, m, n;
    std::string s;
    std::cin >> n >> l >> m >> s;
    if (l != 1) {
        std::sort(s.begin(), s.end(), std::greater<>());
    }
    std::cout << s << std::endl;
}

时间复杂度:\(O(n\log n)\)

空间复杂度:快排的栈空间是\(O(\log n)\)吗?除此之外没有额外空间。

[B] 自平衡二叉查找树

我准备走的时候看见前面那哥们在OI-wiki查平衡二叉树。但是这个题目标题我没看出跟题目有甚么联系。

让编号\(\N\cap[0,n)\)\(n\in\N^*\)个人升序排成一列。将要发一共\(m\in\N^*\)个红包,第\(i\)个在第\(tm_i\)秒发出,有\(mny_i\)块钱,领了这个红包的人要冷却\(cd_i\)秒。每次发红包,由队伍最前面的人领,然后他去冷却,冷却结束后回到队伍中(返回瞬间也可以领红包),队伍总按照编号升序排列。若某个红包发出时,队伍中没有人,则舍弃该红包。计算最终每个人的总收入。

保证\(n,m\leq2\times10^5\)

记录每个人的冷却结束时刻。对于每个红包,我们需要知道从上一个红包到现在有多少人结束了冷却,和所有不在冷却的人里面最小的编号是多少。这两个信息都可以用优先队列维护:一个优先队列按照冷却结束时刻升序排列(冷却队列),另一个按照编号升序排列(空闲队列)。每来一个红包,就将冷却结束时刻小于等于这个红包的发放时刻的人加入空闲队列,让空闲队列的队头(即编号最小的人)领取红包并开始CD(加入冷却队列)。

struct CoolingDown {
    uint64_t tm;
    size_t id;
    inline friend bool operator<(const CoolingDown &lhs, const CoolingDown &rhs) {
        return (lhs.tm > rhs.tm);
    }
};
 
size_t n;
std::vector<uint64_t> income;
std::vector<CoolingDown> cooling;
std::vector<size_t> idle;
std::greater<size_t> gt_idle;
 
inline void preprocess() {
    std::cin >> n;
    income.resize(n);
    idle.resize(n);
    for (size_t i = 0; i != n; ++i) {
        idle[i] = i;
    }
}
 
inline void solve() {
    uint64_t tm, mny, cd;
    std::cin >> tm >> mny >> cd;
    while (cooling.size() && cooling.front().tm <= tm) {
        idle.emplace_back(cooling.front().id);
        std::push_heap(idle.begin(), idle.end(), gt_idle);
        std::pop_heap(cooling.begin(), cooling.end());
        cooling.pop_back();
    }
    if (idle.empty()) {
        return;
    }
    income[idle.front()] += mny;
    cooling.emplace_back(tm + cd, idle.front());
    std::push_heap(cooling.begin(), cooling.end());
    std::pop_heap(idle.begin(), idle.end(), gt_idle);
    idle.pop_back();
}
 
inline void output() {
    for (auto x : income) {
        std::cout << x << '\n';
    }
}

时间复杂度:由于每次发红包最多让一个人进入冷却队列,进出冷却队列的复杂度不超过\(\log n\),故总时间复杂度不超过\(O(m\log n)\)

空间复杂度:\(O(n)\)

[E] 机器人

\(\Z^2\)上,一个机器人从\((0,0)\)出发。给定指令串\(\vec s\in\{\mathtt{'U'},\mathtt{'d'},\mathtt{'L'},\mathtt{'R'}\}^n\ (n\in\N)\),分别表示在坐标平面内向上、下、左、右移动一格。机器人将执行该指令串\(m\)次,求其路径覆盖的点的总数(重复经过同一点只计一次)。

保证\(n\leq2\times10^5,\ m\leq10^{12}\)

不会做。

[J] 伞兵一号准备就绪

给定\(\Z^2\)\(n\in\N^*\)个点\(p\in\Z^{n\times2}\),每个点有价值\(\vec v\in(\N^*)^n\),求\(\max_{\vec i\in\N^4,\ 四边形p_{i_0}p_{i_1}p_{i_2}p_{i_3}构成等腰梯形}\sum_{j=0}^3v_{i_j}\)

保证\(4\leq n\leq1000\),无重复点。

不会做。

[2025-07-23] 位运算和状压DP

[Luogu P1896] [SCOI2005] 互不侵犯

给定\(n\in\N^*,\ m\in\N\),求将\(m\)颗棋子摆放在一个\(n\times n\)的棋盘上,使得以每个棋子为中心的九宫格内(忽略超出边界的部分)没有其他棋子的方案数。

保证\(n\leq9,\ m\leq n^2\)

DP的思路是很好想的,但细节处理上值得揣度,特别是如何枚举状态。

一行的棋子摆放方式可用这一行内放了棋子的位置的集合表示。设\(f_{i,j,S}\)表示在前\(i\)行中,摆一共\(j\)颗棋子,且最后一行的摆放方式为\(S\)的方案数。状态转移方程是

\[f_{i,j,S}=\sum_{摆放S_0与下一行的S不冲突}f_{i-1,j-|S|,S_0} \]

递推边界

\[f_{0,0,\empty}=1 \]

不过,硬枚举\(S_0,S\)太慢了,如何枚举状态才不会TLE呢?如何快速判断枚举的状态是否合法呢?

合法性的判断可以用位运算实现:

  • 行内合法的判断:!(((mask << 1) | (mask >> 1)) & mask)
  • 行间合法的判断:!(((to_mask << 1) | to_mask | (to_mask >> 1)) & frm_mask)

可以先预处理出所有合法的行状态,每次枚举相邻两行的行状态。确保合法后再枚举棋子数。检查棋子数是否合法需要计算popcount,这个也可以预处理。

inline void solve() {
	size_t n, m;
	std::cin >> n >> m;

	auto pop_cnts = std::vector<unsigned>(uint32_t(1) << n);
	for (uint32_t mask = 0; mask != (uint32_t(1) << n); ++mask) {
		pop_cnts[mask] = pop_cnts[mask >> 1] + (mask & 1);
	}

	auto row_masks = std::vector<uint32_t>();
	for (uint32_t mask = 0; mask != (uint32_t(1) << n); ++mask) {
		if (!(((mask << 1) | (mask >> 1)) & mask)) {
			row_masks.emplace_back(mask);
		}
	}

	auto dp = std::vector<std::vector<std::vector<uint64_t>>>(
		n + 1,
		std::vector<std::vector<uint64_t>>(m + 1,
										   std::vector<uint64_t>(1 << n)));
	dp[0][0][0] = 1;
	for (size_t i = 1; i <= n; ++i) {
		for (uint32_t frm_mask : row_masks) {
			for (uint32_t to_mask : row_masks) {
				if (((to_mask << 1) | to_mask | (to_mask >> 1)) & frm_mask) {
					continue;
				}
				for (size_t j = pop_cnts[to_mask]; j <= m; ++j) {
					dp[i][j][to_mask] += dp[i - 1][j - pop_cnts[to_mask]][frm_mask];
				}
			}
		}
	}

	uint64_t res = 0;
	for (uint32_t mask : row_masks) {
		res += dp[n][m][mask];
	}
	std::cout << res << std::endl;
}

时间复杂度:小于\(O(2^{2n}nm)\),因为排除了不合法的行内状态。这个值本身是可以达到\(191102976\approx1.9\times10^8\)的,但由于我们剔除了不合法的行状态,通过行间检验这个比较严格的条件,又筛去了很多情况后,才枚举棋子数,所以实际循环数应该远远小于这个值。

空间复杂度:\(O(2^nnm)\)

[2025-07-27] 分块和莫队

[Luogu P4168] 【Violet】蒲公英

这题目真他妈恶心。还加密输入,加你妈的密!调试到绝望。

给定\(\vec a\in(\N^*)^n\ (n\in\N^*)\),进行\(q\)次查询,每次给定\(p,l\in\N^*,\ 0\leq p<p+l\leq n\),求\(\vec a\)在区间\([p,p+l)\)的众数。

保证\(n\leq40000,\ q\leq50000\)

先将原数组离散化,设一共\(tot\)个不同的数。

设块大小为\(b\),分\(m=\left\lceil\frac{n}{b}\right\rceil\)块。记\(modes_{i,j}\)为块\([i,i+j)\)的众数,\(pref\_sum_{i,j}\)为数\(i\)在前\(j\)块中出现的次数。

uint64_t cnt_testcases;
size_t n, m, b;
std::vector<uint32_t> arr, unq;
std::vector<size_t> rks;
std::vector<std::vector<size_t>> pref_sums; // i appears pref_sums[i][j] times in blocks [0, j)
std::vector<std::vector<uint32_t>> modes;	// mode[i][j] is the first mode in blocks [i, i + j)

预处理

对于每个\(i\),维护一个计数数组\(\overrightarrow{cnt}\in\N^{tot}\),表示当前各个数出现的个数。初始时\(\overrightarrow{cnt}=\vec0\)。使\(j\)遍历\([1,m-i]\),每增加一个数,\(\overrightarrow{cnt}\)维护的区间就需要并上\([(j-1)\cdot b,j\cdot b)\)这一部分,注意增加计数器的同时检查众数有没有改变。这样,我们就在\(O(nm)\)的时间里预处理出了\(modes\)

\(pref\_sum_{i,j}\)很好处理,直接计数然后前缀和即可。时间复杂度:\(O(n+tot\cdot m)\),不超过\(O(nm)\)

inline size_t findMode(size_t pos, size_t len,
					   size_t mode, std::vector<size_t> &cnt) {
	for (size_t i = 0; i < len; ++i) {
		if ((++cnt[rks[pos + i]]) > cnt[mode] ||
			(cnt[rks[pos + i]] == cnt[mode] && rks[pos + i] < mode)) {
			mode = rks[pos + i];
		}
	}
	return mode;
}

inline void preprocess() {
	// Input
	std::cin >> n >> cnt_testcases;
	arr.resize(n);
	for (auto &x : arr) {
		std::cin >> x;
	}

	// Discretize
	unq = discretize(arr, rks);

	// Compute pref_sums
	b = std::sqrt(n);
	pref_sums.resize(unq.size(), std::vector<size_t>((m = (n + b - 1) / b) + 1));
	for (size_t i = 0; i != n; ++i) {
		++pref_sums[rks[i]][i / b + 1];
	}
	for (auto &pref_sum : pref_sums) {
		for (size_t i = 1; i <= m; ++i) {
			pref_sum[i] += pref_sum[i - 1];
		}
	}

	// Compute modes
	modes.resize(m);
	auto cnt = std::vector<size_t>(unq.size());
	for (size_t i = 0; i != m; ++i) {
		modes[i].resize(m - i + 1);
		std::fill(cnt.begin(), cnt.end(), 0);
		for (size_t j = i + 1; j <= m; ++j) {
			// From [i, j - 1) to [i, j)
			modes[i][j - i] = findMode((j - 1) * b, std::min(b, n - (j - 1) * b),
									   modes[i][j - i - 1], cnt);
		}
	}
}

查询

分块问题中,左闭右开的区间其实不如完全的闭区间合适。

如果区间\([p,p+l)\)里面没有整块,就直接暴力求,时间复杂度\(O(b)\)

如果有整块的话,容易想到众数一定来自\(首尾散块\cup\{中间整块的众数\}=a[p,\lceil p/b\rceil)\cup a[\lfloor(p+l-1)/b\rfloor,p+l)\cup modes[\lfloor p/b\rfloor+1,\lfloor(p+l-1)/b\rfloor)\)。要从中选出最终的众数的话,可以先在散块中暴力计数,再分别利用前缀和,加上它们在中间整块中出现的次数即可。时间复杂度不超过\(O(b+\min\{b,tot\})\),不超过\(O(b)\)

inline size_t query(size_t pos, size_t len) {
	if (pos / b + 1 >= (pos + len - 1) / b) {
		auto cnt = std::vector<size_t>(unq.size());
		return findMode(pos, len, 0, cnt);
	}
	std::unordered_map<size_t, size_t> cnt;
	for (size_t i = 0; i < b - pos % b; ++i) {
		++cnt[rks[pos + i]];
	}
	for (size_t i = 0; i < (pos + len - 1) % b + 1; ++i) {
		++cnt[rks[(pos + len - 1) / b * b + i]];
	}
	for (auto &[x, c] : cnt) {
		c += pref_sums[x][(pos + len - 1) / b] - pref_sums[x][pos / b + 1];
	}
	if (!cnt.count(modes[pos / b + 1][(pos + len - 1) / b - (pos / b + 1)])) {
		size_t mode = modes[pos / b + 1][(pos + len - 1) / b - (pos / b + 1)];
		cnt.emplace(mode,
					pref_sums[mode][(pos + len - 1) / b] -
						pref_sums[mode][pos / b + 1]);
	}
	size_t mode = 0, cnt_mode = cnt[mode];
	for (auto &[x, c] : cnt) {
		if (c > cnt_mode || (c == cnt_mode && x < mode)) {
			mode = x;
			cnt_mode = c;
		}
	}
	return mode;
}

时间复杂度:\(O(nm+qb)\)。由于\(n,q\)最大值相近,故可以直接取\(b=\lfloor\sqrt n\rfloor\)。于是得\(m=\lceil\sqrt n\rceil\)

空间复杂度:\(O(n+tot\cdot m+\sqrt n\cdot\sqrt n)\leq O(nm)=O(n\sqrt n)\)

[2025-07-28] BITACM SC 2025 Rating #2

这次的难度比上次翻了几倍呵。

[E] 表格

\(n\in\N^*\)个字符串按字典序排序。

std::sort(strs.begin(), strs.end());

[D] 012

一开始其实看见这个状压DP了,但看别的题搞忘了。其实很简单的。

给定\(a\in\{0,1,2\}^{n\times m}\ (n,m\in\N^*)\),按照如下规则在一个\(n\times m\)的棋盘上放置棋子:

  1. 任意两个棋子不能上下或左右相邻。
  2. 对于\((i,j)\in\N^2\cap([0,n)\times[0,m))\)
    • \(a_{i,j}=0\)则此处该点处可以放棋子,也可以不放棋子。
    • \(a_{i,j}=1\)则该处不可放棋子。
    • \(a_{i,j}=2\)则该处必须放棋子。

求总的放置方案数,答案对\(998244353\)取模。

保证\(n,m\leq10\)

思路参考[Luogu P1896] [SCOI2005] 互不侵犯。

inline void solve() {
	size_t n, m;
	std::cin >> n >> m;
	auto bans = std::vector<uint16_t>(n), musts = std::vector<uint16_t>(n);
	for (size_t i = 0; i != n; ++i) {
		char ch;
		for (size_t j = 0; j != m; ++j) {
			std::cin >> ch;
			if (ch == '1') {
				bans[i] |= (uint16_t(1) << j);
			} else if (ch == '2') {
				musts[i] |= (uint16_t(1) << j);
			}
		}
	}

	std::vector<uint16_t> valid_rows;
	for (uint16_t mask = 0; mask != (uint16_t(1) << m); ++mask) {
		if ((mask & (mask << 1)) || (mask & (mask >> 1))) {
			continue;
		}
		valid_rows.emplace_back(mask);
	}

	auto dp = std::vector<std::vector<uint64_t>>(n + 1, std::vector<uint64_t>(uint16_t(1) << m));
	dp[0][0] = 1;
	for (size_t i = 0; i != n; ++i) {
		for (auto cur : valid_rows) {
			if ((cur & bans[i]) || ((cur & musts[i]) != musts[i])) {
				continue;
			}
			for (auto pre : valid_rows) {
				if (cur & pre) {
					continue;
				}
				dp[i + 1][cur] = (dp[i + 1][cur] + dp[i][pre]) % MOD;
			}
		}
	}

	uint64_t res = 0;
	for (auto row : valid_rows) {
		res = (res + dp[n][row]) % MOD;
	}
	std::cout << res << '\n';
}

时间复杂度:小于\(O(2^{2m}n)\)

空间复杂度:\(O(2^mn)\)

[J] zxj卖篮球

\(n\in\N^*\)个人前来买瓜,第\(i\in\N\cap[0,n)\)人有一个便宜价\(a_i\in\N^*\)和一个底线价\(b_i\in\N^*\)。当瓜老板的定价\(x\leq b_i\)时,第\(i\)个顾客一定会买,但如果不够便宜(\(a_i<x\leq b_i\))的话,他会嫌贵而捅老板一刀;当定价\(x>b_i\)时,顾客会被吓跑,不会买瓜,也不会捅人。现在给定\(m\in\N\cap[0,n]\),由瓜老板自由定价,求其在被捅不超过\(m\)刀的情况下,总收入最多是多少。

保证\(n\leq2\times10^5,\ \forall i\in\N\cap[0,n)\ (a_i<b_i\leq2\times10^9)\)

将顾客按照\(b_i\)降序排序。我们让定价从最高逐渐降低(\(a\)\(b\)都需要考虑哦),对每个价格计算有多少人买、有多少人捅刀,在捅刀数不超过\(m\)时更新答案。

由于定价是逐渐降低的,我们可以用一个大顶堆维护当前价格下嫌贵的人,他们的便宜价是多少。这样,当价格降到便宜价时就可以将其取出。

inline void solve() {
	size_t n, m;
	std::cin >> n >> m;
	auto arr = std::vector<std::pair<uint32_t, uint32_t>>(n);
	for (auto &[a, _] : arr) {
		std::cin >> a;
	}
	for (auto &[_, b] : arr) {
		std::cin >> b;
	}
	std::sort(arr.begin(), arr.end(),
			  [](const auto &a, const auto &b) {
				  return (a.second > b.second);
			  });
	uint64_t res = 0, price = UINT32_MAX;
	size_t cheap = 0;						 // 不觉得贵的人
	std::priority_queue<uint32_t> expensive; // 觉得贵,差评人数
	// 总共有(cheap + expensive.size())个人买
	for (size_t i = 0; i != n; ++i) {
		// 先考虑把价格逐渐降到arr[i].second + 1
		while (expensive.size() > m && expensive.top() > arr[i].second) {
			price = expensive.top();
			expensive.pop();
			++cheap;
		}
		if (expensive.size() <= m) {
			maxEq(res, price * (cheap + expensive.size()));
		}
		// 将价格设为arr[i].second
		price = arr[i].second;
		expensive.push(arr[i].first);
		while (expensive.top() >= price) {
			expensive.pop();
			++cheap;
		}
	}
	while (expensive.size() > m) {
		price = expensive.top();
		expensive.pop();
		++cheap;
	}
	maxEq(res, price * (cheap + expensive.size()));
	std::cout << res << '\n';
}

时间复杂度:\(O(n\log n)\)

空间复杂度:额外的空间不超过\(O(n)\)

[F] 模模meow

他妈的,写了半天C++,不知道怎么回事,取模老写不对。愤以Python秒之。

给定\(n\in\N^*\),求满足\(a\bmod b=c,\ a,b,c,\leq n,\ a\neq b\neq c\neq a\)的三元组\((a,b,c)\in\N^*\)的s数量。

保证\(n\leq10^{12}\)

直接枚举三个数,时间复杂度是\(O(n^3)\)

由于\(a,b\)确定时,\(c\)必确定,所以自由度只有\(2\)。由于要求\(a\neq c\neq 0\),故只需枚举\(a,b\in\N\cap[1,n]\)满足\(a>b,\ (a\bmod b\neq 1\iff b\nmid a)\),这样复杂度就降到\(O(n^2)\)

进一步考虑,如果固定\(b\),那\(\cap(b,n]\)里面有多少个整数不被\(b\)整除?容易想到,答案应为\((n-b)-\left(\left\lfloor\frac{n}{b}\right\rfloor-1\right)\)。之所以要减\(1\),是因为要去掉\(b\)本身。这样,只需枚举\(b\),答案就是\(\sum_{b=1}^n\left((n-b)-\left(\left\lfloor\frac{n}{b}\right\rfloor-1\right)\right)=\frac{1}{2}n(n+1)=\sum_{i=1}^n\left\lfloor\frac{n}{i}\right\rfloor\)。时间复杂度\(O(n)\)

但是此题的\(n\)竟然高达\(10^{12}\),连\(O(n)\)都满足不了他。怎么办?枚举几个数,发现\(i\)比较大时,\(\left\lfloor\frac{n}{i}\right\rfloor\)大量重复,也就是函数\(\frac{n}{x}\)随着\(x\)增大,减慢得越来越慢。由快到慢的分界点就在\(\sqrt n\)处。那末,我们可以考虑枚举\(x=\left\lfloor\frac{n}{i}\right\rfloor\)。一个\(x\)会覆盖那些\(i\)呢?举几个例子就可以注意到(当然也很容易推导),覆盖的\(i\in\left(\left\lfloor\frac{n}{x+1}\right\rfloor,\left\lfloor\frac{n}{x}\right\rfloor\right]\)

推导

\(i\)满足\(\left\lfloor\frac{n}{i}\right\rfloor=x\),则有\(x\leq\frac{n}{i}<x+1\),故有\(\frac{n}{x+1}<i\leq\frac{n}{x}\)。又因为\(i\in\N^*\),故\(i\in\left(\left\lfloor\frac{n}{x+1}\right\rfloor,\left\lfloor\frac{n}{x}\right\rfloor\right]\)

\(\sqrt n\)左边,直接暴力;右边则按照上述方式枚举\(x\),每个\(x\)贡献即为\(\left(\left\lfloor\frac{n}{x}\right\rfloor-\left\lfloor\frac{n}{x+1}\right\rfloor\right)x\)。不过注意分界点处可能会有重复,所以我们枚举左边时,需要枚举到越过分界点且当前值不再重复(将要有与前一项不相等的\(\left\lfloor\frac{n}{i}\right\rfloor\))。

\(n=10^{12}\)时,uint64_t是存不下\(\frac{1}{2}n(n+1)\)的。有减法的取模不知道为甚么一直没写对,非常烦躁,__uint128_t也搞起来麻烦,遂用Py写了一遍。

MOD = 998244353
n = int(input())
res = n * (n + 1) // 2
i0 = 1
while i0 * i0 < n or n // i0 == n // (i0 - 1):
    res -= n // i0
    i0 += 1

x0 = n // i0
for x in range(1, x0 + 1):
    res -= (n // x - n // (x + 1)) * x

print(res % MOD)

时间复杂度:\(O(\sqrt n)\)

空间复杂度:\(O(1)\)

[A] 异或

比赛时是没想出来的,看见异或只知道trie甚么的。

给定\(\vec a\in\N^n\ (n\in\N^*)\),每次操作选择一个\(x\in\vec a\),令\((\forall i\in\N,\ i<n)\ (a_i\gets a_i\oplus x)\)。求有限次(包括\(0\)次)操作后可以得到的最大的\(\sum_{i=0}^{n-1}a_i\)

保证\(n\leq2\times10^5,\ \forall i\in\N\cap[0,n)\ (a_i\leq10^9)\)

因为异或运算具有结合律和归零律(结合起来,\(a\oplus(a\oplus b)=b\))所以可以猜想多次操作无意义,多次与一次等价。

证明

设两次操作分别选择了位置\(x,y\in\N\cap[0,n)\)的数。第一次操作后,变为\((a_i\oplus a_x)_{i=0}^{n-1}\);第二次操作后变为\(((a_i\oplus a_x)\oplus(a_y\oplus a_x))_{i=0}^{n-1}=(a_i\oplus a_y)_{i=0}^{n-1}\)。所以,多次操作等价于一次操作。

接下来我们要求的是,如何选择一个最好的\(x\),使得异或后总和最大呢?朴素的想法是,枚举所有的\(x\),计算\(\sum_{i=0}^{n-1}(a_i\oplus x)\),与\(\sum_{i=0}^{n-1}a_i\)比较,时间复杂度\(O(n^2)\)

以上是我在比赛时想出来了的。接下来参考了大佬的思路。

想要优化,只有两个地方可以下手:一个是枚举\(x\),另一个是计算异或后的总和。枚举\(x\)并没有想到好办法,而计算异或后总和则有以下的好办法。

对于位运算问题,按位分解是非常自然的想法。对于第\(i\)个数的第\(b\)位,异或后对总和的贡献是\(2^b(a_{i,b}\oplus x_b)\)。那末就很好办了:预处理出每一位上值为\(1\)的数的个数。设第\(b\)位上为\(1\)的数一共有\(c_b\)个,一共\(B\)位,则答案就是\(\sum_{b=0}^{B-1}2^b((n-c_b)\text{ if }x_b\text{ else }c_b)\)

inline void solve() {
	size_t n;
	std::cin >> n;
	auto arr = std::vector<uint32_t>(n);
	auto cnt = std::vector<size_t>(32);
	for (auto &x : arr) {
		std::cin >> x;
		for (size_t i = 0; i != 32; ++i) {
			if (x & (1 << i)) {
				++cnt[i];
			}
		}
	}
	uint64_t max = std::accumulate(arr.begin(), arr.end(), uint64_t(0));
	for (auto x : arr) {
		uint64_t sum = 0;
		for (size_t i = 0; i != 32; ++i) {
			if (x & (1 << i)) {
				sum += (n - cnt[i]) << i;
			} else {
				sum += cnt[i] << i;
			}
		}
		maxEq(max, sum);
	}
	std::cout << max << '\n';
}

时间复杂度:\(O(Bn)\)

空间复杂度:额外的空间复杂度\(O(B)\)

[2025-07-30] 概率和组合数学基础

[UVa 11021] Tribles

给定\(m,n,t\in\N\)\(\vec p\in[0,1]^m\),表示:\(0\)时刻时,有\(n\)只tribbles,每只tribble在\(1\)单位时间后变为\(i\in\N\cap[0,m)\)只tribbles的概率为\(p_i\),求\(t\)时刻后死绝的概率。

保证\(m,n,t\leq1000,\ m\neq 0,\ \sum_{i=0}^{m-1}p_i=1\)

每只tribble的变化都是独立的,所以可以考虑每一只tribble的变化,而每个tribble的子代的变化又是各自独立的。由子问题思想,一只tribble在\(x\)时间后死绝的概率,可以由它下一时刻产生的tribbles在\(x-1\)时间后死绝的概率计算。

\(f_i\)为一只tribble在\(i\)时间后死绝的概率。容易得到状态转移方程

\[f_i=\sum_{j=0}^{m-1}p_j\cdot f_{i-1}^j\ (i\in\N,\ i\geq 2) \]

递推边界

\[f_0=0,\ f_1=p_0 \]

inline void solve() {
	size_t n, m, t;
	std::cin >> m >> n >> t;
	auto probabilities = std::vector<double>(m);
	for (auto &p : probabilities) {
		std::cin >> p;
	}
	if (!t) {
		std::cout << "0\n";
		return;
	}
	double res = probabilities[0];
	for (size_t i = 2; i <= t; ++i) {
		double pre = res, pow = 1;
		res = 0;
		for (size_t j = 0; j != m; ++j) {
			res += pow * probabilities[j];
			pow *= pre;
		}
	}
	std::cout << std::fixed << std::setprecision(8) << std::pow(res, n) << '\n';
}

时间复杂度:\(O(tm)\)

空间复杂度:额外空间\(O(1)\)

[Luogu P4316] 绿豆蛙的归宿

给定一个具有\(n\in\N^*\)个顶点、\(m\in\N^*\)条边的带权有向无环图(DAG)。绿豆蛙从顶点\(0\)出发,终点为顶点\(n-1\)。到达某个顶点后,均匀地随机选择该顶点的一条出边跳到下一个点。求从起点到终点所经过路径的总权值的期望。

保证\(n\leq10^5,\ m\leq2n\),第\(i\in\N\cap[0,m)\)条边的权值\(w_i\in\N\cap[0,10^9)\),图无重边。

由期望的线性性,猜测整个路径总权值的期望等于各边被经过的概率与其权值之积的和。

证明

定义指示随机变量\(c_i\ (i\in\N,\ i<m)\)表示第\(i\)条边是否被路径经过。那末路径总权值可以写作

\[W=\sum_{i=0}^{n-1}c_iw_i \]

故其期望

\[\begin{align} \mathrm E(W)&=\sum_{i=0}^{n-1}w_i\mathrm E(c_i)\\ &=\sum_{i=0}^{n-1}w_i(\mathrm P(c_i=0)\cdot 0+\mathrm P(c_i=1)\cdot 1)\\ &=\sum_{i=0}^{n-1}\mathrm P(c_i=1)w_i \end{align} \]

如此,我们只需通过拓扑排序求出各边被经过的概率即可。

这里我将顶点被经过的概率与边被经过的概率都存在了prob中,前\(n\)项为顶点的,后\(m\)项为边的。

size_t n, m;
std::vector<Edge> edges;
std::vector<std::vector<size_t>> adj;

inline void solve() {
	auto indeg = std::vector<size_t>(n);
	for (auto &[_, v, _] : edges) {
		++indeg[v];
	}
	auto prob = std::vector<double>(n + m);
	prob[0] = 1;
	std::queue<size_t> q;
	q.emplace(0);
	while (q.size()) {
		auto frm = q.front();
		q.pop();
		for (auto e : adj[frm]) {
			auto to = edges[e].v;
			prob[to] += (prob[n + e] = prob[frm] / adj[frm].size());
			if (!--indeg[to]) {
				q.emplace(to);
			}
		}
	}
	double res = 0;
	for (size_t i = 0; i != m; ++i) {
		res += prob[n + i] * edges[i].w;
	}
	std::cout << std::fixed << std::setprecision(2) << res << '\n';
}

时间复杂度:\(O(n+m)\)

空间复杂度:\(O(n+m)\)

[2025-08-04] BITACM SC 2025 Rating #3

怎么全他妈是数论,数论我一点不会啊ToT

[I] 如来

给定\(n\in\mathbb N^*\),表示有一张由\(n\)个顶点(编号从\(1\)开始)组成的无向图,对于两个顶点\(i,j\),它们之间有一条边当且仅当\(\gcd(i,j)=i\),且边长为\(|j-i|\)。现给定阈值\(m\),进行\(q\in\mathbb N^*\)次询问,每次询问给定\(x,y\in\mathbb N^*\cap[1,n]\)满足\(x\neq y\),判断是否顶点\(x\)\(y\)间的最短路的长度是否超过\(m\)

保证\(n,q\leq10^5\)

每条边描述一个整除关系,即两个数之间有边当且仅当一个数能被另一个整除。

现在我们要考虑两个数\(x,y\ (x<y)\)之间的最短路。当\(x\mid y\)时,显然最短路就是\(y-x\)。不整除时,我们就需要经过中间节点。如果只有一个中间节点,那这个数要么是\(x,y\)的公约数,要么就是他们的公倍数,这两种情况里面最大公约数和最小公倍数分别是最优的。我的直觉猜测仅经过最大公约数就是最优的,但比赛时没有证明。

证明

引理:\(\forall a,b\in\mathbb N^*\ (a+b\leq\gcd(a,b)+\mathrm{lcm}(a,b))\)

证明

\(g=\gcd(a,b),\ \alpha=a/g,\ \beta=b/g\),则

\[a=\alpha g,\ b=\beta g,\ \mathrm{lcm}(a,b)=\alpha\beta g \]

要证\(a+b\leq\gcd(a,b)+\mathrm{lcm}(a,b)\),即证

\[(\alpha+\beta)g\leq(\alpha\beta+1)g \]

\(g\geq1\),即证

\[\alpha\beta-\alpha-\beta+1\geq0 \]

因式分解,即证

\[(\alpha-1)(\beta-1)\geq0 \]

\(a,b\in\mathbb N^*\),所以\(\alpha,\beta\geq1\),所以上式成立,原式得证。

\(x,y\in\mathbb N^*\)

如果一条路径依次经过点\(\vec c\in(\mathbb N^*\cap[1,n])^{m+2}\ (m\in\mathbb N^*)\),其中\(c_0=x,\ c_{m+1}=y\),且存在\(i_0\in\mathbb N\cap[1,m]\)满足\((\forall i\in\mathbb N^*\cap[1,i_0]\ (c_{i}\mid c_{i-1}))\wedge(\forall i\in\mathbb N^*\cap[i_0,m]\ (c_i\mid c_{i+1}))\),则称这条路径为\(x,y\)间的一条V形路径,称\(c_{i_0}\)为他的谷底。

\(x,y\)间存在以\(v\)为谷底的V形路径,当且仅当\(v\)\(x,y\)的公约数,即\(v\mid x,\ v\mid y\)\(x,y\)间任何一条以\(v\)为谷底的V形路径,其长度必为\((x-2v+y)\)。所以当\(v=\gcd(x,y)\)时得到\(x,y\)间的最短V形路径。

如果\(x,y\)间一条路径不是V形路径,则必存在\(i_0\in\mathbb N\cap[1,m]\)满足\(c_{i_0-1}<c_{i_0}>c_{i_0+1}\),即存在一个“山峰”。此时有\(c_{i_0-1}\mid c_{i_0},\ c_{i_0+1}\mid c_{i_0}\),即\(c_{i_0}\)\(c_{i_0-1},c_{i_0+1}\)的公倍数。由引理得\(c_{i_0-1}\)\(c_{i_0+1}\)之间的路径长度

\[2c_{i_0}-c_{i_0-1}-c_{i_0+1}\geq2\mathrm{lcm}(c_{i_0-1},c_{i_0+1})-c_{i_0-1}-c_{i_0+1}\geq c_{i_0-1}+c_{i_0+1}-2\gcd(c_{i_0-1},c_{i_0+1}) \]

所以用公约数代替公倍数一定不更劣,且大多数时候会更优。因此,\(x,y\)间的最优路径一定是V形路径。

由上述证明可知\(x,y\)间最短路长度即为\(x+y-\gcd(x,y)\)

时间复杂度:\(O(q\log n)\)

空间复杂度:\(O(1)\)

[B] 排列

询问\(q\)次,每次给定\(m,n\in\mathbb N^*\),求\(\mathbb N\cap[1,n]\)有多少种排列满足恰有\(m\)个数归位,答案对\((10^9+7)\)取模。

保证\(q\leq2\times10^5,\ m\leq n\leq10^6\)

思路是很直接的,答案就是

\[\binom{n}{m}\mathrm D_{n-m} \]

其中\(\mathrm D_n\)表示错排数。这个值可以递推算出。

关键在于预处理以便快速计算\(\binom{n}{m}\)。当\(n\)较小时,可以利用递推式

\[\binom{n}{m}=\binom{n-1}{m-1}+\binom{n-1}{m} \]

预先计算所有的组合数,不过这需要\(O(n^2)\)的空间来存储所有结果。更好的方式是预处理阶乘和阶乘逆,然后直接利用组合数的计算式

\[\binom{n}{m}=\frac{n!}{m!(n-m)!} \]

计算即可。

constexpr uint64_t MOD = 1e9 + 7, N = 1e6;
std::array<uint64_t, N + 1> derangements, facts, fact_invs;

inline void preprocess() {
	derangements[0] = 1;
	for (uint32_t i = 1; i <= N; ++i) {
		derangements[i] = i * derangements[i - 1] % MOD;
		if (i & 1) {
			derangements[i] = (derangements[i] + MOD - 1) % MOD;
		} else {
			if ((++derangements[i]) == MOD) {
				derangements[i] = 0;
			}
		}
	}

	facts[0] = facts[1] = 1;
	fact_invs[0] = MOD;
	fact_invs[1] = 1;
	for (uint32_t i = 2; i <= N; ++i) {
		facts[i] = i * facts[i - 1] % MOD;
		fact_invs[i] = (MOD - MOD / i) * fact_invs[MOD % i] % MOD; // invs[i]
	}
	for (uint32_t i = 2; i <= N; ++i) {
		fact_invs[i] = fact_invs[i] * fact_invs[i - 1] % MOD;
	}
}

uint64_t comb(uint64_t n, uint64_t m) {
	if (!m || m == n) {
		return 1;
	}
	return ((facts[n] * fact_invs[m] % MOD) * fact_invs[n - m] % MOD);
}

inline void solve() {
	size_t n, m;
	std::cin >> n >> m;
	if (m > n) {
		m = n;
	}
	// comb(n, m) * derangements(n - m)
	std::cout << (comb(n, m) * derangements[n - m] % MOD) << '\n';
}

[J] 鱼鱼爱小数

事实上是个很简单的题,但由于我是大傻逼,所以脑残根本没想到快速幂。

给定正整数\(x,y,n\),求\(\frac{l}{r}\)在十进制下小数点后的第\(n\)位到第\(n+2\)位(从\(1\)开始数)。

保证\(x,y,n\leq10^9\)

即求

\[\left\lfloor\frac{x}{y}\cdot10^{n+2}\right\rfloor\bmod10^3 \]

我的思路是直接模仿高精度除法,不断除以\(y\),得到余数后,末尾补\(0\)(即乘\(10\))进行下一次迭代。这在\(n=10^9\)的数量级下很可能超时。当时我认为这是数据卡常数导致的,因为我本地测需要\(4\ \mathrm s\)左右。遂采取了神秘的卡常奇技淫巧Barrett reduction来加速对\(y\)取模的运算,得到了本地\(2\ \mathrm s\)、评测机AC的好成绩。本地比评测机慢可能是因为我本地环境是WSL 2。

inline void solve() {
	uint32_t lhs, rhs, n;
	std::cin >> lhs >> rhs >> n;
	auto barrett = Barrett<>(rhs);
	lhs %= rhs;
	for (uint32_t i = 1; i < n; ++i) {
		lhs = barrett(lhs, 10);
	}
	std::array<unsigned, 3> res;
	for (unsigned i = 0; i != 3; ++i) {
		res[i] = lhs * 10 / rhs;
		lhs = barrett(lhs, 10);
	}
	for (auto &x : res) {
		std::cout << x;
	}
}

时间复杂度:\(O(n)\)

空间复杂度:\(O(1)\)

赛后才知道正解如此简单。

\[\left\lfloor\frac{x}{y}\cdot10^{n+2}\right\rfloor\bmod10^3=((10^{n+2}\cdot x)\bmod(10^3\cdot y))/y \]

证明

\(x,y,m\in\mathbb N^*,\ q=x/y/m,\ r=x/y-mq\),则

\[x/y=mq+r \]

两边同乘\(y\),得

\[qmy+ry\leq x<qmy+ry+y \]

所以

\[ry\leq x\bmod(my)<(r+1)y \]

同时除以\(y\),得

\[r\leq\frac{x\bmod(my)}{y}<r+1 \]

向下取整,

\[r\leq(x\bmod(my))/y<r+1 \]

所以

\[r=(x\bmod(my))/y \]

inline void solve() {
	uint64_t lhs, rhs, n;
	std::cin >> lhs >> rhs >> n;
	uint64_t mod = rhs * 1000;
	std::cout << std::setw(3) << std::setfill('0') << (qPowMod(10, n + 2, mod) * lhs % mod / rhs) << std::endl;
}

时间复杂度:\(O(\log n)\)

空间复杂度:\(O(1)\)

[2025-08-10] BITACM SC 2025 Rating #4

[F] 签到题

给定\(n\in\mathbb N^*\)个字符串和\(m,l\in\mathbb N^*\),判断能否从他们的所有回文子串(相等的不合并)中恰好选出\(m\)个,使其长度之和为\(l\)

不超过\(10\)组测试,保证\(n,m,l\leq100\)

因为数据范围很小,可以考虑用比较暴力的办法。

对每个字符串用Manacher算法计算出所有回文子串的长度,合并在一起形成\(\overrightarrow{len}\in(\mathbb N^*)^t\ (t\in\mathbb N^*)\),于是原问题就转化为,判断有没有一种取法,从\(t\)个数中取\(m\)个,使其总和为\(l\)

DP是呼之欲出的,没想到DP写个记忆化搜索也是一样的。设\(f_{i,j,k}\)表示在前\(i\)个数中取\(m\)个使其总和为\(k\)的方案数,于是有状态转移方程

\[f_{i,j,k}=f_{i-1,j,k}+ \begin{cases} 0&(k<len_{i-1})\\ f_{i-1,j-1,k-len_{i-1}}&(k\geq len_{i-1}) \end{cases} \]

递推边界

\[f_{i,0,0}=1 \]

这里应该可以采用滚动数组优化掉一维的空间。

inline void solve() {
	size_t n, m, l;
	std::cin >> n >> m >> l;
	std::vector<size_t> lens;
	lens.reserve(2 * n * n);
	for (size_t i = 0; i != n; ++i) {
		std::string s;
		std::cin >> s;
		auto [odd, even] = manacher(s.begin(), s.end());
		for (auto len : odd) {
			for (size_t i = 1; i <= len; ++i) {
				lens.emplace_back(2 * i - 1);
			}
		}
		for (auto len : even) {
			for (size_t i = 1; i <= len; ++i) {
				lens.emplace_back(2 * i);
			}
		}
	}
	auto dp = std::vector<std::vector<std::vector<uint64_t>>>(
		lens.size() + 1,
		std::vector<std::vector<uint64_t>>(m + 1, std::vector<uint64_t>(l + 1)));
	dp[0][0][0] = 1;
	for (size_t i = 0; i < lens.size(); ++i) {
		dp[i + 1][0][0] = 1;
		for (size_t j = 1; j <= m; ++j) {
			for (size_t k = 1; k <= l; ++k) {
				dp[i + 1][j][k] = dp[i][j][k] +
								  (k >= lens[i] ? dp[i][j - 1][k - lens[i]] : 0);
			}
		}
	}
	std::cout << (dp[lens.size()][m][l] ? "YES\n" : "NO\n");
}

假设字符串长度均为\(s\),则\(t=2ns\)

时间复杂度:\(O(nsml)\)

空间复杂度:\(O(nsml)\)。采用滚动数组优化后可以减少到\(O(ml)\)

[B] 拼回文签到

给定长为\(n\)的字符串\(\vec s\),求通过选取\(\vec s\)不相交的前缀与后缀各一个拼接起来能得到的回文串的最大长度。

多组数据,保证\(\sum n\leq5000\)

这是A.题的弱化版。在这个数据范围下,\(O(n^2)\)的时间复杂度也是可以接受的,所以我们可以先跑一遍Manacher算法,然后在外层循环中枚举相同长度的前后缀长度,里层再枚举回文串中心位置,如果有这么一个回文串能够摸到我们枚举的前缀或者后缀,就记录答案。不过有很多细节需要注意,特别注意要避免这三段有重叠。

inline void solve() {
    std::string s;
    std::cin >> s;
    auto [odd, even] = manacher(s.begin(), s.end());
    size_t res = 0;
    for (size_t i = 0; 2 * i <= s.size(); ++i) {
        if (i && s[i - 1] != s[s.size() - i]) {
            break;
        }
        maxEq(res, 2 * i);
        for (size_t j = i; j + i < s.size(); ++j) {
            if (j < i + odd[j] || j + odd[j] >= s.size() - i) {
                maxEq(res, 2 * i + std::min(2 * odd[j] - 1,
                                            std::min(2 * (j - i) + 1,
                                                     2 * (s.size() - i - j) - 1)));
            }
            if (j <= i + even[j] || j + even[j] >= s.size() - i) {
                maxEq(res, 2 * i + std::min(2 * even[j],
                                            std::min(2 * (j - i),
                                                     2 * (s.size() - i - j))));
            }
        }
    }
    std::cout << res << '\n';
}

时间复杂度:\(O(n^2)\)

空间复杂度:\(O(n)\)

[A] 拼回文

给定长为\(n\)的字符串\(\vec s\),求通过选取\(\vec s\)不相交的前缀与后缀各一个拼接起来能得到的最长的回文串中的任意一个。

多组数据,保证\(\sum n\leq10^6\)

在B题的基础上,A题不仅增大了数据规模,而且要求记录完整的字符串,这不仅提高了优化要求,而且需要注意的细节更多。

可以利用贪心思想优化B题给出的解法。我们可以先贪心地(也就是尽量长地)寻找相等前后缀,再枚举回文串中心,而不是套成循环。

证明

按照B题思路,假设外层循环到第\(i\)次时,内层找到的最优解是一个挨着前缀的以\(j\)为中心的长为\(l\)回文串。

如果不相等,则这一轮循环的答案就是\(2i+l\)

如果\(s_i=s_{n-i}\),如果我们把\(i\)\(n-i\)分别加到前后缀中,最终得到的回文串总长为\(2(i+1)+(l-2)=2i+l\),如果不加也是\(2i+l\),所以加不会比不加更差,但加入前后缀等于就是直接进行了下一次的循环。所以只需把相等前后缀一次性枚举到底即可。

方便起见,可以在求出最大的\(i\)后,只对\(s_{[i,n-i)}\)做Manacher。细节是真多。

inline void solve() {
    std::string s;
    std::cin >> s;
    size_t i = 0;
    for (; 2 * i + 1 < s.size(); ++i) {
        if (s[i] != s[s.size() - 1 - i]) {
            break;
        }
    }
    auto res = s.substr(0, ((2 * i > s.size()) ? (i - 1) : i)) + s.substr(s.size() - i);
    auto [odd, even] = manacher(s.begin() + i, s.end() - i);
    for (size_t j = 0; j != odd.size(); ++j) {
        if (2 * i + 2 * odd[j] - 1 > res.size()) {
            if (j + 1 == odd[j]) {
                res = s.substr(0, i + 2 * odd[j] - 1) + s.substr(s.size() - i);
            } else if (j + odd[j] == odd.size()) {
                res = s.substr(0, i) + s.substr(s.size() - i - (2 * odd[j] - 1));
            }
        }
        if (2 * i + 2 * even[j] > res.size()) {
            if (j == even[j]) {
                res = s.substr(0, i + 2 * even[j]) + s.substr(s.size() - i);
            } else if (j + even[j] == even.size()) {
                res = s.substr(0, i) + s.substr(s.size() - i - 2 * even[j]);
            }
        }
    }
    std::cout << res << '\n';
}

时间复杂度:\(O(n)\)

空间复杂度:\(O(n)\)

[E] 你要进入……吗

给定\(\vec a\in\mathbb N^n\ (n\in\mathbb N)\),求\(\max_{b_0=0}^{n-2}\max_{e_0=b_0+1}^{n-1}\max_{b_1=e_0}^{n-1}\max_{e_1=b_1}^n\left(\left(\bigoplus_{i=b_0}^{e_0-1}a_i\right)+\left(\bigoplus_{i=b_1}^{e_1-1}a_i\right)\right)\)

保证\(2\leq n\leq10^6,\ \forall i\in\mathbb N\cap[0,n)\ (a_i\leq10^9)\)

暴力法是\(O(n^5)\)的时间复杂度。让我们一步步优化。

首先,最容易想到的就是前缀和优化。由异或运算的消去律,可以通过\(O(n)\)预处理出前缀异或和,以便\(O(1)\)快速查询区间异或和。设前缀异或和

\[\vec s=\left(\bigoplus_{j=0}^{i-1}a_i\right)_{i=0}^n \]

于是

\[原式=\max_{b_0=0}^{n-2}\max_{e_0=b_0+1}^{n-1}\max_{b_1=e_0}^{n-1}\max_{e_1=b_1}^n\left((s_{e_0}\oplus s_{b_0})+(s_{e_1}\oplus s_{b_1})\right) \]

接下来,由于对\([b_0,e_0)\)\([b_1,e_1)\)的处理是相同的,我们考虑将其统一起来。设

\[f(\vec s,m)=\max_{b=0}^{m-1}\max_{e=b+1}^m(s_e\oplus s_b)=\max_{i=0}^{n-1}\max_{j=0}^{i-1}(s_i\oplus s_j) \]

\[原式=\max_{m=1}^{n-1}(f(\vec s,m)+f(\mathrm{rev\ }\vec s,n-m)) \]

那这个\(f(\vec s,m)\max_{i=0}^{n-1}\max_{j=0}^{i-1}(s_i\oplus s_j)\)如何快速求呢?如果固定\(i\),则\(\max_{j=0}^{i-1}(s_i\oplus s_j)\)可以通过字典树在\(O(\log\max\vec s)\)的时间内求出。那么我们可以随着\(i\)增大,每次把\(s_i\)加入字典树,并求\(\max_{j=0}^{i-1}(s_i\oplus s_j)\),这样就可以在\(O(n\log\max\vec s)\)的时间复杂度下求出\(\overrightarrow{first}=(f(\vec s,m))_{m=0}^n\)\(\overrightarrow{last}=(f(\mathrm{rev\ }\vec s,m))_{m=0}^{n}\)

有了这两个结果,答案就手到擒来了:

\[原式=\max_{m=1}^{n-1}(first_m+last_{n-m}) \]

struct Node {
	uint32_t cnt_end = 0;
	std::array<uint32_t, 2> children;
};

constexpr size_t N = 1e6;
size_t n, m = 1;
std::array<uint32_t, N + 1> arr, first, last;
std::array<Node, 32 * N> nodes;

inline void insert(uint32_t val) {
	size_t p = 0;
	for (unsigned i = 31; ~i; --i) {
		bool b = val & (uint32_t(1) << i);
		if (!nodes[p].children[b]) {
			nodes[p].children[b] = (m++);
		}
		p = nodes[p].children[b];
	}
	++nodes[p].cnt_end;
}

inline uint32_t query(uint32_t val) {
	size_t p = 0;
	uint32_t res = 0;
	for (unsigned i = 31; ~i; --i) {
		bool b = val & (uint32_t(1) << i);
		res = (res << 1) | (nodes[p].children[!b] ? !b : b);
		p = nodes[p].children[res & 1];
	}
	return res;
}

inline void clear() {
	for (size_t i = 0; i != m; ++i) {
		nodes[i].cnt_end = 0;
		nodes[i].children[0] = 0;
		nodes[i].children[1] = 0;
	}
	m = 1;
}

inline void solve() {
	std::cin >> n;
	++n;
	for (size_t i = 1; i < n; ++i) {
		std::cin >> arr[i];
		arr[i] ^= arr[i - 1];
	}
	for (size_t i = 0; i != n; ++i) {
		insert(arr[i]);
		first[i] = query(arr[i]) ^ arr[i];
		if (i && first[i] < first[i - 1]) {
			first[i] = first[i - 1];
		}
	}
	clear();
	for (size_t i = 0; i != n; ++i) {
		insert(arr[n - i - 1]);
		last[i] = query(arr[n - i - 1]) ^ arr[n - i - 1];
		if (i && last[i] < last[i - 1]) {
			last[i] = last[i - 1];
		}
	}
	uint64_t res = 0;
	for (size_t i = 1; i < n; ++i) {
		maxEq(res, static_cast<uint64_t>(first[i] + last[n - i]));
	}
	std::cout << res << std::endl;
}

时间复杂度:\(O(n\log\max\vec a)\)

空间复杂度:\(O(n\log\max\vec a)\)

这里时空都会卡常数,所以你可以看到我难得地全用的静态内存……一开始是动态开点的字典树,智能指针写的,TLE;改成静态的,当时Node里写的还是size_t,于是又MLE了……

[2025-08-16] BITACM SC 2025 Final

[J] 并查集?

初始时给定\(n\in\mathbb N^*\)个集合:\(\{1\},\{2\},\dots,\{n\}\)。接下来要求你进行\(q\in\mathbb N^*\)个操作,操作一共分为三类:

  1. 将元素\(x\)所在的集合和元素\(y\)所在的集合合并。
  2. 将元素\(x\)移动到元素\(y\)所在的集合中。
  3. 查询元素\(x\)所在集合的大小以及集合中所有元素之和。

特别要注意的是,在操作1和2中,如果\(x,y\)已经在同一个集合之中,那就无视这次操作。

本题中包含多个测试样例,保证\(n,q\leq10^5,\ \sum n,\sum q\leq1.5\times10^6\)

操作1和3都是很典型的并查集操作,所以我们主要关注操作2。我的想法是,将\(x\)移动到\(y\)所在集合,但可能有别的元素把\(x\)当作老大,所以可以建立一个新的点\(x'\)来替代\(x\),此后凡操作\(x\)一律操作\(x'\),但通过别的点访问到的\(x\)依然按原\(x\)来。由于\(n,q\)同阶,所以完全可以接受。我们只需维护一个数组\(\overrightarrow{msk}\in(\mathbb N^*)^n\)表示真正的元素所在即可。

class Dsu {
public:
    Dsu(size_t n = 0) : m_msk(n), m_fa(n), m_sz(n, 1), m_sum(n) {
        for (size_t i = 0; i != n; ++i) {
            m_msk[i] = i;
            m_fa[i] = i;
            m_sum[i] = i;
        }
    }
 
    size_t find(size_t x) {
        x = m_msk[x];
        while (m_fa[m_fa[x]] != m_fa[x]) {
            m_fa[x] = m_fa[m_fa[x]];
        }
        return m_fa[x];
    }
 
    void merge(size_t from, size_t to) {
        m_fa[from = find(from)] = (to = find(to));
        m_sz[to] += m_sz[from];
        m_sz[from] = m_sz[to];
        m_sum[to] += m_sum[from];
        m_sum[from] = m_sum[to];
    }
 
    void move(size_t x, size_t to) {
        size_t f = find(x);
        --m_sz[f];
        m_sum[f] -= x;
        m_msk[x] = size();
        m_fa.emplace_back(m_fa.size());
        m_sz.emplace_back(1);
        m_sum.emplace_back(x);
        merge(x, to);
    }
 
    size_t size() const { return m_fa.size(); }
 
    size_t sizeOf(size_t x) { return m_sz[find(x)]; }
 
    size_t sumOf(size_t x) { return m_sum[find(x)]; }
 
protected:
    std::vector<size_t> m_msk, m_fa, m_sz;
    std::vector<uint64_t> m_sum;
 
private:
};
 
inline void solve(size_t n, size_t m) {
    Dsu dsu(n + 1);
    int op;
    size_t x, y;
    while (m--) {
        std::cin >> op >> x;
        if (op == 3) {
            std::cout << dsu.sizeOf(x) << ' ' << dsu.sumOf(x) << '\n';
        } else {
            std::cin >> y;
            if (dsu.find(x) == dsu.find(y)) {
                continue;
            }
            if (op == 1) {
                dsu.merge(x, y);
            } else if (op == 2) {
                dsu.move(x, y);
            } else {
                assert(0);
            }
        }
    }
}

时间复杂度:\(O((n+q)\log(n+q))\)

空间复杂度:\(O(n+q)\)

[I] 沈阳大街

给定\(n,m\in\mathbb N\),构造\(\mathbb N\cap[1,n]\)的排列\(\vec a=(a_i)_{i=1}^n\),使得\(|\{i\in\mathbb N\cap[1,n]:\gcd(i,a_i)>1\}|=m\)。若无解则输出\(-1\)

保证\(1\leq n\leq10^5,\ 0\leq m\leq n\)

  1. \(m=n\)时肯定无解,因为\(i=1\)时必然\(\gcd(i,a_i)=1\)
  2. \(m=n-1\)时,直接\(a_i=i\)即可。
  3. \(m=n-2\)时,可以交换\(a_1\)\(a_n\),剩下的都\(a_i=i\)即可。

这个第三点至关重要,它会启发我们的子问题思想。交换\(a_1\)\(a_n\)会让我们剩下考虑的区间变成\([2,n-1]\),这是不好的。要是可以变成\([1,n-1]\)或者\([1,n-2]\)这样就好了。有没有这样的办法呢?

\(\gcd(i,a_i)>1\)是容易的,因为当\(i>1\)时必有\(\gcd(i,i)>1\)。于是我们考虑想办法让\(\gcd(i,a_i)=1\)。因为\(i\)\((i+1)\)必然互质,所以我们可以在有需要的时候直接交换相邻两个元素,从而创造两个\(\gcd(i,a_i)=1\)的位置。那末问题就很简单了。

设当前的问题是\((n,m)\)。当\(m=n\)\(m=n-1\)时,直接按照上述方式构造即可。当\(m<n-1\)时,我们可以通过令\((a_{n-1},a_n)=(n,n-1)\)让问题规模变为\((n-2,m)\)。这样,我们就可以递归地解决这个问题了。当然实际上我们完全不需要写成递归形式。

inline void solve() {
    uint32_t n, m;
    std::cin >> n >> m;
    if (n <= m) {
        std::cout << "-1\n";
        return;
    }
    std::vector<uint32_t> perm(n + 1);
    while (n > m + 2) {
        perm[n] = n - 1;
        perm[n - 1] = n;
        n -= 2;
    }
    for (uint32_t i = 1; i <= n; ++i) {
        perm[i] = i;
    }
    if (n == m + 2) {
        std::swap(perm[1], perm[n]);
    }
    for (uint32_t i = 1; i < perm.size(); ++i) {
        std::cout << perm[i] << ' ';
    }
    std::cout << std::endl;
}

时间复杂度:\(O(n)\)

空间复杂度:可以\(O(1)\)。不过比赛时懒得考虑这么多,写了个数组方便一点。

[K] 项链

给定\(\vec a,\vec b\in\mathbb N^n\ (n\in\mathbb N^*)\),求\(\max_{d\in\mathbb Z}\max_{e\in\mathbb Z}\max_{i=0}^{n-1}\sum_{j=0}^{n-1}((b_j+e)-(a_{(i+j)\bmod n}+d))^2\)

保证\(n\leq10^5,\ \forall i\in\mathbb N\cap[0,n)\ (a_i\leq100)\)

要求将\(\vec a,\vec b\)分别加上增量\(d,e\)后的“残差平方和”,所以显然\(d,e\)只需要保留一个就可以了。这里我们保留\(d\),并且为了表示方便,不妨令\(\vec a\gets(\vec a,\vec a)\),于是

\[\begin{align*} 原式&=\max_{d\in\mathbb Z}\max_{i=0}^{n-1}\sum_{j=0}^{n-1}(b_j-a_{i+j}-d)^2\\ &=\max_{d\in\mathbb Z}\max_{i=0}^{n-1}\left(nd^2-2\left(\sum_{j=0}^{n-1}(b_j-a_{i+j})\right)d+\sum_{j=0}^{n-1}(b_j-a_{i+j})^2\right)\\ &=\max_{d\in\mathbb Z}\left(nd^2-2\left(\sum\vec b-\sum\vec a\right)d\right)+\max_{i=0}^{n-1}\sum_{j=0}^{n-1}(b_j-a_{i+j})^2 \end{align*} \]

于是第一项的最大值就可以通过二次函数性质快速求出。于是问题的关键变为求\(\max_{i=0}^{n-1}\sum_{j=0}^{n-1}(b_j-a_{i+j})^2\)

当时我完全没看懂给这个\(a_i\leq100\)到底是为甚么。在比赛的绝望时刻,我又一次产生了神秘的做法。

那就是直接暴力求。因为在这个数据量下,暴力会非常慢,所以我用尽了我能想到的一切卡常技巧。

  • 开全局数组。
  • 因为\((b_j-a_{i+j})^2=a_{i+j}^2-2a_{i+j}b_j+b_j^2\),真正的\(i,j\)混合项只在交叉项上,所以可以先单独求\(\vec a^2\)\(\vec b^2\)。求这两项应当会比较快,因为缓存命中率极高。
  • 中间的交叉项就暴力求。为了免于取模,我们拆成\(\max_{i=0}^{n-1}\left(\sum_{j=0}^{n-i-1}a_{i+j}b_j+\sum_{j=n-i}^{n-1}a_{i+j-n}b_j\right)\)
constexpr size_t N = 1e5;
int32_t n, m;
std::array<int32_t, N> a, b;

inline void preprocess() {
	std::cin >> n >> m;
	for (int32_t i = 0; i != n; ++i) {
		std::cin >> a[i];
	}
	for (int32_t i = 0; i != n; ++i) {
		std::cin >> b[i];
	}
}

inline int32_t sumDiff() {
	int32_t res = 0;
	for (int32_t i = 0; i != n; ++i) {
		res += b[i];
	}
	for (int32_t i = 0; i != n; ++i) {
		res -= a[i];
	}
	return res;
}

inline int32_t minAfterAdd() {
	int32_t sd = sumDiff(), d = sd / n;
	if (sd >= 0) {
		if (2 * sd > n * (2 * d + 1)) {
			++d;
		}
	} else if (2 * sd < n * (2 * d - 1)) {
		--d;
	}
	return (n * d * d - 2 * sd * d);
}

inline int32_t minRss() {
	int32_t res = 0;
	for (int32_t i = 0, j; i != n; ++i) {
		int32_t sum = 0;
		for (j = 0; j + i != n; ++j) {
			sum += a[j + i] * b[j];
		}
		for (; j != n; ++j) {
			sum += a[j + i - n] * b[j];
		}
		if (res < sum) {
			res = sum;
		}
	}
	res = -2 * res;
	for (int32_t i = 0; i != n; ++i) {
		res += a[i] * a[i];
	}
	for (int32_t i = 0; i != n; ++i) {
		res += b[i] * b[i];
	}
	return res;
}

inline void solve() {
	std::cout << (minAfterAdd() + minRss()) << std::endl;
}

在本地测试时,我开了O2,生成了一个\(n=10^5\)的大样例,跑完竟然只要\(2.5\ \mathrm s\)。喜出望外,提交至牛客,TLE。遂查询“牛客OJ开O2优化”,于是加上

#pragma GCC optimize(2)

结果仍然TLE。又在本地测O3,竟然只要\(1.6\ \mathrm s\),怒而改为

#pragma GCC optimize(3,"Ofast","inline")

提交,于是AC。

时间复杂度:\(O(n^2)\),离谱。

空间复杂度:\(O(1)\)额外空间。

这把全靠编译器带飞。赛后研究汇编,发现在O3下它生成了一大堆我从未见过的指令,例如

.L4:
        movdqu  xmm0, XMMWORD PTR [rsi+rax]
        movdqa  xmm2, XMMWORD PTR b[rax]
        add     rax, 16
        movdqa  xmm1, xmm0
        psrlq   xmm2, 32
        pmuludq xmm1, XMMWORD PTR b[rax-16]
        pshufd  xmm1, xmm1, 8
        psrlq   xmm0, 32
        pmuludq xmm0, xmm2
        pshufd  xmm0, xmm0, 8
        punpckldq       xmm1, xmm0
        paddd   xmm3, xmm1
        cmp     rax, rdx
        jne     .L4
        movdqa  xmm0, xmm3
        psrldq  xmm0, 8
        paddd   xmm3, xmm0
        movdqa  xmm0, xmm3
        psrldq  xmm0, 4
        paddd   xmm3, xmm0
        movd    edx, xmm3
        test    r8b, 3
        je      .L5
        mov     eax, r8d
        and     eax, -4
        lea     ecx, [r9+rax]
.L8:
        movdqu  xmm0, XMMWORD PTR [r14+rax]
        movdqu  xmm2, XMMWORD PTR [r12+rax]
        add     rax, 16
        movdqa  xmm1, xmm0
        psrlq   xmm0, 32
        pmuludq xmm1, xmm2
        psrlq   xmm2, 32
        pmuludq xmm0, xmm2
        pshufd  xmm1, xmm1, 8
        pshufd  xmm0, xmm0, 8
        punpckldq       xmm1, xmm0
        paddd   xmm3, xmm1
        cmp     rcx, rax
        jne     .L8
        movdqa  xmm0, xmm3
        psrldq  xmm0, 8
        paddd   xmm3, xmm0
        movdqa  xmm0, xmm3
        psrldq  xmm0, 4
        paddd   xmm3, xmm0
        movd    eax, xmm3
        add     edx, eax
        test    r13b, 3
        je      .L6
        and     r13d, -4
        add     ebp, r13d

那一堆p开头的指令是我从未见过的。据Gemini分析,这里是将数组向量化后进行并行计算。太聪明!编译器我爱你!

posted @ 2025-08-18 14:25  我就是蓬蒿人  阅读(33)  评论(0)    收藏  举报