2025牛客寒假算法基础集训营1


A. 茕茕孑立之影

题意:给你n个数,你要找一个数使得这个数和数组的任意一个数都不成倍数关系。

如果数组里有1肯定不行,1是所有数的因子。其他情况我们只需要找一个大质数就行,因为值域只有1e9,可以输出1e9+7

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::vector<int> a(n);
	int ans = 1e9 + 7;
	for (int i = 0; i < n; ++ i) {
		std::cin >> a[i];
	}

	std::sort(a.begin(), a.end());
	if (a[0] == 1) {
		std::cout << -1 << "\n";
	} else {
		std::cout << ans << "\n";
	}
}

B. 一气贯通之刃

题意:给你一颗树,要你找一条简单路径经过所有点。

如果这颗树不是一条链的话,不可能找到一条路径经过所有点。所以判断是不是链,然后找链的两端就行。
可以用度数判断,度数为一个点连的边数量。一条链上的点度数都小于等于2,并且两端的点度数是1

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::vector<int> deg(n);
	for (int i = 1; i < n; ++ i) {
		int u, v;
		std::cin >> u >> v;
		-- u, -- v;
		++ deg[u];
		++ deg[v];
	}

	std::vector<int> ans;
	int cnt = 0;
	for (int i = 0; i < n; ++ i) {
		if (deg[i] == 1) {
			ans.push_back(i);
		}

		cnt += deg[i] == 2;
	}

	if (cnt != n - 2 || ans.size() != 2) {
		std::cout << -1 << "\n";
	} else {
		std::cout << ans[0] + 1 << " " << ans[1] + 1 << "\n";
	}
}

C. 兢兢业业之移

题意:给你一个01矩阵,你要把所有的1都移动到左上部分。给出方案。

直接枚举所有左上部分的点,我们按行从上到下,按列从左到右枚举。那么如果我们枚举到了(i,j),则所有1<=x<i,1<=y<=n/2的地方以及x=i,1<=y<j的地方全都是1,如果这个(i,j)0,我们找一个不在已经操作好的位置的1的位置移过来就行了。

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::vector<std::string> s(n);
	for (int i = 0; i < n; ++ i) {
		std::cin >> s[i];
	}

	std::vector<std::array<int, 4> > ans;
	for (int i = 0; i < n / 2; ++ i) {
		for (int j = 0; j < n / 2; ++ j) {
			if (s[i][j] == '0') {
				int x = -1, y = -1;
				for (int l = 0; l < n && x == -1; ++ l) {
					for (int r = 0; r < n && x == -1; ++ r) {
						if ((l < i && r < n / 2) || (l == i && r < j)) {
							continue;
						}
						if (s[l][r] == '1') {
							x = l, y = r;
							break;
						}
					}
				}

				while (x < i) {
					std::swap(s[x][y], s[x + 1][y]);
					ans.push_back({x, y, x + 1, y});
					++ x;
				}

				while (y < j) {
					std::swap(s[x][y], s[x][y + 1]);
					ans.push_back({x, y, x, y + 1});
					++ y;
				}

				while (y > j) {
					std::swap(s[x][y], s[x][y - 1]);
					ans.push_back({x, y, x, y - 1});
					-- y;
				}

				while (x > i) {
					std::swap(s[x][y], s[x - 1][y]);
					ans.push_back({x, y, x - 1, y});
					-- x;
				}
			}
		}
	}

	std::cout << ans.size() << "\n";
	for (auto & [a, b, c, d] : ans) {
		std::cout << a + 1 << " " << b + 1 << " " << c + 1 << " " << d + 1 << "\n";
	}
}

D. 双生双宿之决

题意:一个数组是双生数组,那么它恰好有两种元素,并且每一种元素的个数是n/2。判断给你的数组是不是双生数组。

统计判断即可。

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::map<int, int> mp;
	for (int i = 0; i < n; ++ i) {
		int x;
		std::cin >> x;
		++ mp[x];
	}

	std::vector<int> a;
	for (auto & [x, y] : mp) {
		a.push_back(y);
	}

	if (a.size() != 2 || a[0] != a[1]) {
		std::cout << "No\n";
	} else {
		std::cout << "Yes\n";
	}
}

E. 双生双宿之错

题意:一个数组是双生数组,那么它恰好有两种元素,并且每一种元素的个数是n/2,你每次可以让数组一个元素加一或者减一,求让数组变成双生数组的最小操作数。

先排序,因为只有两个数组并且每个都是一半,那么我们分成左半和右半来操作。每一边都要变成一个相同的数。变成两边的中位数是最优的。但可能两边中位数一样,那么我们枚举两边中位数减少或增大就行了。
为什么中位数最优?具体的来说,我们设在中间位置左边的所有点,到中位数的差之和为p,右边的差之和则为q那么我们就必须让p+q的值尽量小。当位置向左移动的话,p会减少x但是q会增加nx.所以说当为数组中位数的时候,p+q最小。

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::vector<i64> a(n + 1);
	for (int i = 1; i <= n; ++ i) {
		std::cin >> a[i];
	}

	if (n == 2) {
		if (a[1] == a[2]) {
			std::cout << 1 << "\n";
		} else {
			std::cout << 0 << "\n";
		}
		return;
	}

	std::sort(a.begin(), a.end());
	if (a[1] == a[n]) {
		std::cout << n / 2 << "\n";
		return;
	}

	i64 midl = a[n / 2 / 2], midr = a[n / 2 + n / 2 / 2];
	i64 ans = 1e18;
	for (i64 x = midl - 10; x <= midl + 10; ++ x) {
		for (i64 y = midr - 10; y <= midr + 10; ++ y) {
			if (x == y) {
				continue;
			}
			i64 sum = 0;
			for (int i = 1; i <= n; ++ i) {
				if (i <= n / 2) {
					sum += std::abs(a[i] - x);
				} else {
					sum += std::abs(a[i] - y);
				}
			}

			ans = std::min(ans, sum);
		}
	}

	midl = a[n / 2 / 2 + 1];
	midr = a[n / 2 + n / 2 / 2 + 1];
	for (i64 x = midl - 10; x <= midl + 10; ++ x) {
		for (i64 y = midr - 10; y <= midr + 10; ++ y) {
			if (x == y) {
				continue;
			}
			i64 sum = 0;
			for (int i = 1; i <= n; ++ i) {
				if (i <= n / 2) {
					sum += std::abs(a[i] - x);
				} else {
					sum += std::abs(a[i] - y);
				}
			}

			ans = std::min(ans, sum);
		}
	}

	std::cout << ans << "\n";
}

F. 双生双宿之探

题意:双生数组的定义和E题一样。求有多少子数组是双生数组。

考虑双指针枚举每个只有两个数的区间。那么我们确定了双生数组的两个数。那么就可以单独求这个区间的贡献。设两个数是x,y,当ai等于x时加1,否则减1。那么就变成求一个前缀和和当前相等。

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::vector<int> a(n);
	for (int i = 0; i < n; ++ i) {
		std::cin >> a[i];
	}

	i64 ans = 0;
	for (int i = 0; i < n; ++ i) {
		int j = i + 1;
		std::map<int, int> mp;
		++ mp[a[i]];
		int x = a[i], y;
		while (j < n) {
			mp[a[j]] ++ ;
			if (mp.size() > 2) {
				break;
			}

			if (a[j] != x) {
				y = a[j];
			}

			++ j;
		}

		if (mp.size() == 1) {
			break;
		}

		std::map<int, int> cnt;
		++ cnt[0];
		int sum = 0;
		for (int k = i; k < j; ++ k) {
			if (a[k] == x) {
				++ sum;
			} else {
				-- sum;
			}

			ans += cnt[sum];
			++ cnt[sum];
		}

		for (int k = j - 1; k >= i; -- k) {
			if (a[k] != a[j - 1]) {
				i = k;
				break;
			}
		}
	}

	std::cout << ans << "\n";
}

G. 井然有序之衡

题意:给你一个数组,你每次可以给一个数加一同时给另一个数减一。问能不能变成一个排列,求最小操作数。

将数组排序后,那么应该时最小的变成1,第二小的变成2 ... 最大的变成n。那么模拟即可。
因为每次操作不会改变数组总和,那么一个排列的总和位n(n+1)2,判断数组总和是不是这个数就行了。不是则不可能变成。

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::vector<int> a(n);
	for (int i = 0; i < n; ++ i) {
		std::cin >> a[i];
	}

	if (std::accumulate(a.begin(), a.end(), 0ll) != (i64)n * (n + 1) / 2) {
		std::cout << -1 << "\n";
		return;
	}

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

	i64 ans = 0;
	for (int i = 0, j = n - 1; i < j;) {
		if (a[i] == i + 1) {
			++ i;
		} else if (a[j] == j + 1) {
			-- j;
		} else {
			i64 t = std::min(i + 1 - a[i], a[j] - (j + 1));
			ans += t;
			a[i] += t;
			a[j] -= t;
		}
	}
	std::cout << ans << "\n";
}

H. 井然有序之窗

题意:有一个排列,现在告诉你每个位置数的范围,你要构造一个合适的排列。

我们按照右端点从大到小给,每次尽量给最小的。因为如果有一个区间l更小并且长度小于当前区间但在它后面,那么他可选的数更多,我们不应该先关心它。否则在前面,已经考虑过了。用一个set维护没选过的数即可。

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::vector<std::array<int, 3> > a(n);
	for (int i = 0; i < n; ++ i) {
		int l, r;
		std::cin >> l >> r;
		a[i] = {r, l, i};
	}

	std::sort(a.begin(), a.end());
	std::vector<int> ans(n);

	std::set<int> s;
	for (int i = 1; i <= n; ++ i) {
		s.insert(i);
	}

	for (auto & [r, l, i] : a) {
		auto it = s.lower_bound(l);
		if (it == s.end() || *it > r) {
			std::cout << -1 << "\n";
			return;
		}

		ans[i] = *it;
		s.erase(it);
	}

	for (int i = 0; i < n; ++ i) {
		std::cout << ans[i] << " \n"[i == n - 1];
	}
}

I. 井然有序之桠

题意:给你一个排列,你要构造一个排列,使得gcd(ai,pi)=k

这种构造题真的是靠智商吧。。。
jiangly的视频时很震撼,秒出思路就算了,居然还可以直接想到两个特殊情况。不过jiangly的递归写法确实非常好,这里我们先不管怎么想特殊情况,讲讲一些我们能想到的思路。
首先我们不关注a是怎么排列的,因为两个排列的数是一一对应的,所以我们考虑排列为{1,2,...,n1,n}时该怎么构造。
我们知道,使用分治法的前提时我们可以把原问题分成多个和原问题等价的规模更小的问题,那么我们将问题改为求gcd(pi,i)=k的解。1i的排列能产生的最小值是i,那么只要i1大于等于ki,我们给i这个位置填i,就变成一个可行的更小的问题:求gcd(pi1,i1)=ki的解,然后结尾加上一个i就行;否则,我们可以求gcd(pi2,i2)=k2的解,在末尾加上i,i1就行。这样就变成更小的子问题,当n=0或者n=1的时候我们就可以轻松解决。
这个思路似乎很好,但这个题还有两个特殊情况,如果不通过大量试样例或者打表找规律,我肯定是想不到的。如果我们遇到我们想出来一个很好的方法还是无法通过的情况,可以打表找特例。
这里,用下面的代码发现好像只有两个样例,如果没发现特例是有规律的,那就直接特判再交一发看看。

点击查看代码
for (int i = 1; i <= 5000; ++ i) {
		for (int j = i; j < i + i - 1; ++ j) {
			if (j - 2 > (i - 2) * (i - 1) / 2) {
				std::cout << i << " " << j << "\n";
			}
		}
	}

下面是参考jiangly代码的递归写法。

点击查看代码
std::vector<int> get(int n, i64 k) {
	std::vector<int> p;
	if (n == 0) {

	} else if (n == 1) {
		p = {1};
	} else if (k >= n + n - 1) {
		p = get(n - 1, k - n);
		p.push_back(n);
	} else if (n == 3 && k == 4) {
		p = {3, 2, 1};
	} else if (n == 4 && k == 6) {
		p = {3, 4, 1, 2};
	} else {
		p = get(n - 2, k - 2);
		p.push_back(n);
		p.push_back(n - 1);
	}

	return p;
}

void solve() {
	int n;
	i64 k;
	std::cin >> n >> k;
	std::vector<int> a(n);
	for (int i = 0; i < n; ++ i) {
		std::cin >> a[i];
	}

	if (k < n) {
		std::cout << -1 << "\n";
		return;
	}

	auto p = get(n, k);
	for (int i = 0; i < n; ++ i) {
		std::cout << p[a[i] - 1] << " \n"[i == n - 1];
	}
}

J. 硝基甲苯之袭

题意:给你一个数组,有多少对(i,j)满足gcd(ai,aj)=aiaj

因为值域很小,那么我们枚举每个数的约数d,然后判断gcd(ai,aid)是不是等于d就行了。从左到右计算,记录前面每个数的数量。

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::vector<int> a(n);
	for (int i = 0; i < n; ++ i) {
		std::cin >> a[i];
	}

	const int N = 2e5 + 5;
	std::vector<std::vector<int> > factor(N);
	for (int i = 1; i < N; ++ i) {
		for (int j = i; j < N; j += i) {
			factor[j].push_back(i);
		}
	}

	std::sort(a.begin(), a.end());
	std::map<int, int> cnt;
	i64 ans = 0;
	for (int i = 0; i < n; ++ i) {
		for (auto & j : factor[a[i]]) {
			if (std::gcd(a[i], a[i] ^ j) == j) {
				ans += cnt[a[i] ^ j];
			}
		}

		cnt[a[i]] += 1;
	}

	std::cout << ans << "\n";
}

K. 硝基甲苯之魇

题意:给你一个数组,求有多少区间满足区间gcd等于区间异或和。

因为gcd最大操作log次,所以如果固定右端点r,那么左边最多有loggcd不同的区间[li,r]
并且连续做gcd的值时递减的,这提示我们按gcd去找左端点,如果li是第一个使得[li,r]的区间gcdx的位置,那么可以二分向左找最左边等于区间和等于x。假设得到[p,li]这些点作为左端点到r的区间和都是x,记sumi1i的异或和,那么我们要找sumj1=sumix并且在[p,li]之间的位置。区间gcd可以用线段树维护,记录每个前缀异或和的位置可以用map记录。
因为最多跳log,所以加上线段树的log时间复杂度就是O(nlog2n)

点击查看代码
#define ls (u << 1)
#define rs (u << 1 | 1)
#define umid (tr[u].l + tr[u].r >> 1)

struct Node {
	int l, r;
	int d;
};

struct SegmentTree {
	std::vector<Node> tr;
	SegmentTree(int _n) {
		tr.assign(_n << 2, {});
		build(1, _n);
	}

	void pushup(int u) {
		tr[u].d = std::gcd(tr[ls].d, tr[rs].d);
	}

	void build(int l, int r, int u = 1) {
		tr[u] = {l, r};
		if (l == r) {
			return;
		}

		int mid = l + r >> 1;
		build(l, mid, ls); build(mid + 1, r, rs);
	}

	void modify(int p, int x) {
		int u = 1;
		while (tr[u].l != tr[u].r) {
			int mid = umid;
			if (p <= mid) {
				u = ls;
			} else {
				u = rs;
			}
		}

		tr[u].d = x;
		u >>= 1;
		while (u) {
			pushup(u);
			u >>= 1;
		}
	}

	int query(int l, int r, int u = 1) {
		if (l <= tr[u].l && tr[u].r <= r) {
			return tr[u].d;
		}

		int mid = umid;
		if (r <= mid) {
			return query(l, r, ls);
		} else if (l > mid) {
			return query(l, r, rs);
		}

		return std::gcd(query(l, r, ls), query(l, r, rs));
	}
};

void solve() {
	int n;
	std::cin >> n;
	std::vector<int> a(n);
	std::vector<int> sum(n + 1);
	SegmentTree tr(n);
	std::map<int, std::vector<int> > pos;
	pos[0].push_back(0);
	for (int i = 0; i < n; ++ i) {
		std::cin >> a[i];
		sum[i + 1] = sum[i] ^ a[i];
		pos[sum[i + 1]].push_back(i + 1);
		tr.modify(i + 1, a[i]);
	}

	i64 ans = 0;
	for (int i = 1; i <= n; ++ i) {
		int j = i - 1;
		while (j) {
			int d = tr.query(j, i);
			int l = 1, r = j;
			while (l < r) {
				int mid = l + r >> 1;
				if (tr.query(mid, i) == d) {
					r = mid;
				} else {
					l = mid + 1;
				}
			}

			int t = sum[i] ^ d;
			int L = std::lower_bound(pos[t].begin(), pos[t].end(), l - 1) - pos[t].begin();
			int R = std::upper_bound(pos[t].begin(), pos[t].end(), j - 1) - pos[t].begin() - 1;
			if (L <= R) {
				ans += R - L + 1;
			}

			j = l - 1;
		}
	}

	std::cout << ans << "\n";
}

L. 一念神魔之耀

题意:n盏灯,每次可以选择连续的xy盏灯切换他们的开关,求一个方案使得最后所有灯都是打开的。

我们可以操作任何长度为d=gcd(x,y)的区间,这里假设x>y。那么如果我们想操作[l,l+d1],就先操作[l,l+x1],这样就多出来xd个额外被翻转的,我们要把它们翻回来,那么一直翻转右边的长度为y的区间,最后如果翻转到一个都不剩,就完成了,如果还有剩下一点长度k(k<y),使得无法翻转,那么就操作[l+d+k,l+d+k+x],这样相当于加上去了2x减去了若干个y,一直重复这个操作,其实就是求需要减去多少y和加上几个x可以正好使得多出来的翻转回来,这就是求ax+by=d,根据裴蜀定理,这是一定有解的。那么对于[1,ny+1]这一段,如果是0,就直接翻转就行,最后只剩下,[ny,n]这一段,按照操作gcd的方案模拟即可。

点击查看代码
void solve() {	
	int n, x, y;
	std::cin >> n >> x >> y;
	std::string s;
	std::cin >> s;

	if (x < y) {
		std::swap(x, y);
	}

	std::vector<std::array<int, 2> > ans;
	auto op = [&](int l, int r) {
		ans.push_back({l, r});
		for (int i = l; i <= r; ++ i) {
			s[i] ^= 1;
		}
	};

	int d = std::gcd(x, y);
	for (int i = 0; i + y - 1 < n; ++ i) {
		if (s[i] == '0') {
			op(i, i + y - 1);
		}
	}

	for (int i = n - 1; i + y - 1 >= n; -- i) {
		if (s[i] == '0') {
			op(i - x + 1, i);
			int l = i - x + 1;
			while (1) {
				int j = l;
				while (j + y - 1 <= i - d) {
					op(j, j + y - 1);
					j += y;
				}

				if (j == i - d + 1) {
					break;
				}

				op(j - x, j - 1);
				l = j - x;
			}
		}
	}

	for (auto & c : s) {
		if (c == '0') {
			std::cout << -1 << "\n";
			return;
		}
	}

	std::cout << ans.size() << "\n";
	for (auto & [l, r] : ans) {
		std::cout << l + 1 << " " << r + 1 << "\n";
	}
}

M. 数值膨胀之美

题意:给你一个数组,你要恰好执行一次操作,选择一段区间让这个区间的数都乘2。最小化极差。

要让影响极差,那么我们肯定要改最大值和最小值,发现改最大值只会变大。那么我们应该操作最小值。随便找一个最小值的位置,然后两边扩展,看是不是最小值然后乘2即可。要用set实时维护最大最小值。

点击查看代码
void solve() {
	int n;
	std::cin >> n;
	std::vector<i64> a(n);
	std::multiset<i64> s;
	for (int i = 0; i < n; ++ i) {
		std::cin >> a[i];
		s.insert(a[i]);
	}

	i64 ans = 1e18;
	for (int i = 0; i < n; ++ i) {
		if (a[i] == *s.begin()) {
			s.extract(a[i]);
			s.insert(a[i] * 2);
			ans = std::min(ans, *s.rbegin() - *s.begin());
			int l = i - 1, r = i + 1;
			while (l >= 0 || r < n) {
				if (l >= 0 && a[l] == *s.begin()) {
					s.extract(a[l]);
					s.insert(a[l] * 2);
					ans = std::min(ans, *s.rbegin() - *s.begin());
					-- l;
				} else if (r < n && a[r] == *s.begin()) {
					s.extract(a[r]);
					s.insert(a[r] * 2);
					ans = std::min(ans, *s.rbegin() - *s.begin());
					++ r;
				} else {
					break;
				}
			}
			break;
		}
	}

	std::cout << ans << "\n";
}
posted @   maburb  阅读(463)  评论(10编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示