A

构造

使用[1,2n][1,2n][12n]的元素构成一个长度nnn的数组,,然后求相邻元素和,得到一个长度n−1n-1n1的数组,接到原数组后面。要求操作后数组不含重复元素

一个比较巧妙的构造是,注意到如果我们等差数列构造,等差数列是很容易出现相邻两项和等于后面的项的,所以考虑插空,在奇偶位置分别插入两个等差数列。

这个的一个构造方案是,奇数位ai=ia_i=iai=i,偶数位ai=n+ia_i=n+iai=n+i,这样相邻元素和是一个n+3,n+5..n+3,n+5..n+3,n+5..的等差数列,而我们插入的是1,3,5...1,3,5...1,3,5...n+2,n+4,n+6...n+2,n+4,n+6...n+2,n+4,n+6...的两个等差数列,刚好错开了。

void solve() {
	int n;
	cin >> n;

	rep(i, 1, n) {
		if (i % 2) {
			cout << i << ' ';
		} else {
			cout << n + i << ' ';
		}
	}
	cout << '\n';
}

但这其实做麻烦了,这是aaa题,大道至简,不该有太复杂的做法。注意到限制使用的元素不超过2n2n2n,但是相邻元素和可以超过2n2n2n,那么我们让相邻元素和都大于2n2n2n,不就肯定和原始序列不重复了吗?想让相邻元素和都大于2n2n2n,那么每个元素都至少是n+1n+1n+1,还要不重复,那么就[n+1,2n][n+1,2n][n+1,2n]填进去就好,正好nnn

void solve() {
	int n;
	cin >> n;
	for (int i = 2 * n; i > n; i--) {
		cout << i << ' ';
	}
	cout << '\n';

}

B

贪心/dp

在一个序列中选择一个子序列,全部加上一个正整数kkk,问能否把序列变成非降序?

首先是应该加多少?至少要加max⁡(ai−ai+1)\max(a_i-a_{i+1})max(aiai+1),也就是最大的下降跨度。这样才能在每一个局部下降的地方改成非降序。但这就够了吗?

可以大胆猜想,这就够了。因为,这个kkk,已经够每个下降的位置变成平的了,如果还有下降,说明ai>ai+1a_i\gt a_{i+1}ai>ai+1这样的位置,ai,ai+1a_i,a_{i+1}ai,ai+1都被选入增加的子序列了,都加了kkk,那此时不论kkk多大都没用的,两个位置同步加,永远保持降序关系。

所以可以认为加的值就是k=max⁡(ai−ai+1)k=\max(a_i-a_{i+1})k=max(aiai+1)。操作固定了,问能否升序,显然可以直接贪心,这是常见思路,需要增加的地方(也就是降序的地方)就加,如果增加了,还是降序的,肯定就不能排序了,直接退出。

void solve() {
	int n;
	cin >> n;
	vi a(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}

	int k = 0;
	rep(i, 1, n - 1) {
		k = max(k, a[i] - a[i + 1]);
	}

	if (k == 0) {
		cout << "yes\n";
		return;
	}

	int f = 0;
	rep(i, 2, n) {
		if (a[i] < a[i - 1]) {
			a[i] += k;
		}
		if (a[i] < a[i - 1]) {
			cout << "no\n";
			return;
		}
	}

	cout << "yes\n";

}

前面是直接在原数组上操作的思路,也可以不在原数组上直接操作,而是记录前面的位置是否增加了,然后同样也是看,这个位置增加后能否超过前一位。

  • 如果前一位ai−1a_{i-1}ai1比原始比当前aia_iai大,也就是存在降序,且ai−1a_{i-1}ai1已经被选中要加了,那么肯定就无解了,因为aia_iai即使也加,也还是小于ai−1a_{i-1}ai1
  • 如果aia_iaiai−1a_{i-1}ai1至少大kkk,不论前一位有没有加,都还是升序的,那么此时aia_iai加不加都行,贪心的想,类似LISLISLIS为例让后面更可能升序,结尾应该尽量小,所以这里选择不加。
void solve() {
	int n;
	cin >> n;
	vi a(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}

	int k = 0;
	rep(i, 1, n - 1) {
		k = max(k, a[i] - a[i + 1]);
	}

	if (k == 0) {
		cout << "yes\n";
		return;
	}

	int f = 0;
	//f记录之前的结尾,在最好情况下是否 +k
	rep(i, 2, n) {
		if (a[i] < a[i - 1]) {
			if (f) {
				cout << "no\n";
				return;
			} else {
				f = 1;
			}
		} else if (f && a[i] >= a[i - 1] + k) {
			f = 0;
		}
	}

	cout << "yes\n";

}

能贪,其实也能dpdpdp,只是对于div2bdiv 2bdiv2b来说有点大炮打蚊子了。

第二种贪心的时候,需要考虑前一个位置是否加kkk了,根据贪心策略规定,在能不加kkk的情况下都不加。如果想不到这点,可以用一个状态机dpdpdpf(i,0/1)f(i,0/1)f(i,0/1)表示前缀[1,i][1,i][1,i]升序,且最后一个位置iii是否加kkk,的方案是否存在。

转移类似,如果ai−1>aia_{i-1}\gt a_iai1>ai,必须是i−1i-1i1不加,iii加。如果ai−1<aia_{i-1}\lt a_iai1<ai,但相差不到kkk,如果i−1i-1i1加了,iii就不能加。如果i−1i-1i1没加,iii加不加都行。如果升序,且相差超过kkk,不论前一个加不加,当前加不加都可以。

void solve() {
	int n;
	cin >> n;
	vi a(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}

	int k = 0;
	rep(i, 1, n - 1) {
		k = max(k, a[i] - a[i + 1]);
	}

	if (k == 0) {
		cout << "YES\n";
		return;
	}

	vvi f(n + 1, vi(2));
	//f(i,0/1)表示能否前i个有序,且第i个是/否被选中
	f[1][0] = f[1][1] = 1;
	rep(i, 2, n) {
		if (a[i - 1] > a[i]) {
			f[i][1] = f[i - 1][0];
		} else {
			if (a[i] - a[i - 1] < k) {
				f[i][1] = f[i - 1][0] | f[i - 1][1];
				f[i][0] = f[i - 1][0];
			} else {
				f[i][1] = f[i][0] = f[i - 1][0] | f[i - 1][1];
			}
		}
	}

	if (f[n][1] || f[n][0]) {
		cout << "YES\n";
	} else {
		cout << "NO\n";
	}
}

C

图论 结论 模拟

一个序列,每次可以选一个位置,如果是奇数,加一,如果是偶数,除二。问至少操作多少次,把整个序列变成全都相同?

一个比较显然的结论是,每个数在整个操作下,最终会变成111,并且在变成111之前,执行的操作次数是O(log⁡V)O(\log V)O(logV)的,最多大约60次。

并且这里不存在随机性,每个数一直操作,产生的序列是完全确定的,这个序列最后会进入2,1,2,1...2,1,2,1...2,1,2,1...的循环。

这样,可以把数看成点,这个序列看成图上的一条链,最后进入2,12,12,1构成的环,多个数,构成的是一个2,12,12,1为中心的基环树。问的这个树上选一个点,其他所有点到这个点距离最短是多少?

由于2,12,12,1都在环上,可以相互转换,所以最后都变成1,21,21,2都可以,不妨设都变成111,那么把每个元素,一直操作,直到变成111,得到的所有序列,计算最长公共后缀,这个后缀开头元素,就是想让操作次数最少,应该变成的元素,每个序列长度O(log⁡V)O(\log V)O(logV)nnn个序列一块求最长公共后缀,暴力做复杂度也只有O(nlog⁡V)O(n\log V)O(nlogV)

如果以222作为序列结尾,也同理,但答案可能不同,分别求一下取最值

void solve() {
	int n;
	cin >> n;
	vi a(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}

	auto cal = [&](int v)->int{
		int tot = 0;
		vvi b(n + 1);
		rep(i, 1, n) {
			vi t;
			int x = a[i];
			while (x != v) {
				t.push_back(x);
				if (x % 2)x++;
				else x >>= 1;
			}
			t.push_back(x);
			tot += t.size();

			ranges::reverse(t);
			b[i] = t;
		}

		rep(i, 1, 100) {
			set<int>s;
			bool f = 0;
			rep(j, 1, n) {
				if (b[j].size() < i) {
					f = 1;
					break;
				} else {
					s.insert(b[j][i - 1]);
				}
			}
			if (f || s.size() > 1) {
				break;
			}
			tot -= n;
		}
		return tot;
	};

	cout << min(cal(1), cal(2)) << '\n';
}

另一个更暴力的做法是,考虑我们前面的图论转化,问题等价于,求每个点,到所有初始元素的距离之和的最值。那么可以在计算每个初始元素的序列时,维护它到每个点的距离。

这样计算完所有序列,每个点作为终点的答案也算好了。这样做的话,由于值域很大,需要用mapmapmap存每个点的答案,mapmapmap里一共O(nlog⁡V)O(n\log V)O(nlogV)个点,每次插入复杂度O(log⁡(nlog⁡V))O(\log(n\log V))O(log(nlogV)),一共插入O(nlog⁡n)O(n\log n)O(nlogn)次。复杂度还是很大的,需要卡常才能过,比如换成哈希表。

这个方法实现时,需要注意,即使一个元素初始就是111了,它也可以移动到222,需要更新它到222的距离。并且,一个点,只有全部nnn个点都能到达,才能作为候选答案,所以不止需要统计每个点到初始元素距离和,还要统计这个点有多少个初始元素可以到达。

void solve() {
	int n;
	cin >> n;
	vi a(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}

	unordered_map<int, pii>mp;

	rep(i, 1, n) {
		int x = a[i];
		if (x == 1) {
			mp[2].fi++;
			mp[2].se++;

			mp[1].fi++;
		} else {
			int d = 0;
			while (x != 1) {
				mp[x].fi++;
				mp[x].se += d++;
				if (x % 2)x++;
				else x >>= 1;
			}
			mp[x].fi++;
			mp[x].se += d;
		}
	}

	int ans = inf;
	for (auto &[k, v] : mp) {
		if (v.fi == n) {
			ans = min(ans, v.se);
		}
	}

	cout << ans << '\n';


}

上述做法,有一个优化点:既然每个点只有全部nnn个点都能到,才能作为候选答案,那么任选一个元素,操作形成的序列,终点肯定也在这些点中,所以只用随机选一个序列,统计序列上的点,到达所有初始元素的距离,这样就把统计的点数降低到了O(log⁡V)O(\log V)O(logV),这很小,用哈希表的话,基本没有哈希碰撞,可以视为O(1)O(1)O(1)插入,查询。那么再算上操作次数,整体复杂度也只有O(nlog⁡V)O(n\log V)O(nlogV),跑得很快。

实测比前一个O(nlog⁡V)O(n\log V)O(nlogV)个点快十倍左右,这个才是正解,不需要担心卡常。
在这里插入图片描述
比前面的求最长公共后缀也快一倍左右快
在这里插入图片描述

void solve() {
	int n;
	cin >> n;
	vi a(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}

	unordered_map<int, pii>mp;

	int x = a[1];
	if (x == 1) {
		mp[2].fi++;
		mp[2].se++;

		mp[1].fi++;
	} else {
		int d = 0;
		while (x != 1) {
			mp[x].fi++;
			mp[x].se += d++;
			if (x % 2)x++;
			else x >>= 1;
		}
		mp[x].fi++;
		mp[x].se += d;
	}

	rep(i, 2, n) {
		int x = a[i];
		if (x == 1) {
			mp[2].fi++;
			mp[2].se++;

			mp[1].fi++;
		} else {
			int d = 0;
			while (x != 1) {
				if (mp.count(x)) {
					mp[x].fi++;
					mp[x].se += d;
				}
				d++;
				if (x % 2)x++;
				else x >>= 1;
			}
			mp[x].fi++;
			mp[x].se += d;
		}
	}

	int ans = inf;
	for (auto &[k, v] : mp) {
		if (v.fi == n) {
			ans = min(ans, v.se);
		}
	}

	cout << ans << '\n';


}

D

思维 维护合法区间

一个初始数组aaabbbaaa的前缀和,cccbbb的前缀max⁡\maxmax,现在给出ccc,和部分缺失的aaa。问能否还原出一个合法的aaa

这种给出前缀和,前缀max⁡\maxmax,还原原始数组的问题,一个经典做法是,维护每个位置的可能值域,如果每个位置的值域最终都没有矛盾,则可以在区间内任取,然后推出整个原始数组。

具体就是:

  • 如果ci=ci−1c_i=c_{i-1}ci=ci1bib_ibi的取值限制是只要不超过bib_ibi就行
  • 如果ci>ci−1c_i\gt c_{i-1}ci>ci1bib_ibi就是前缀最大,bi=cib_i=c_ibi=ci,值域直接缩到一个点。
  • 另外,如果si=1s_i=1si=1,也就是aia_iai这个位置的值是准确的,那么bib_ibi的取值区间,至少是bi−1b_{i-1}bi1的区间整体加aia_iai
  • 第三点和前两点的约束要同时生效,也就是把区间取交集。
  • c1=b1=a1c_1=b_1=a_1c1=b1=a1一定成立,递推初始状态可以直接确定。

这样可以推出每个位置的取值范围,检查无矛盾,则有解。

具体构造方案,考虑递推计算,bnb_nbn的值可以直接在区间内任选,中间的值,假设bi+1b_{i+1}bi+1已经确定,bib_ibi的值要在[li,ri][l_i,r_i][li,ri]内取,并且如果si+1=1s_{i+1}=1si+1=1,也就是ai+1a_{i+1}ai+1是确定的,那么bi=bi+1−ai+1b_i=b_{i+1}-a_{i+1}bi=bi+1ai+1一定成立,没有选择余地,必须是这个值。如果没有这个严格约束,可以在[li,ri][l_i,r_i][li,ri]任取,直接取rir_iri即可。

还原出bbb后差分即可得到aaa

void solve() {
	int n;
	cin >> n;

	string s;
	cin >> s;
	vi a(n + 1), c(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}

	rep(i, 1, n) {
		cin >> c[i];
	}

	vector<pii>lim(n + 1);

	rep(i, 2, n) {
		if (c[i] < c[i - 1]) {
			cout << "no\n";
			return;
		}
	}

	if (s[0] == '1' && a[1] != c[1]) {
		cout << "no\n";
		return;
	}

	lim[1].fi = lim[1].se = c[1];

	rep(i, 2, n) {
		int l, r;
		if (c[i] == c[i - 1]) {
			l = -inf;
			r = c[i];
		} else {
			l = r = c[i];
		}

		if (s[i - 1] == '1') {
			lim[i].fi = max(lim[i - 1].fi + a[i], l);
			lim[i].se = min(lim[i - 1].se + a[i], r);
		} else {
			lim[i].fi = l;
			lim[i].se = r;
		}
	}

	vi b(n + 1);
	b[n] = lim[n].se;
	rep1(i, n, 1) {
		if (lim[i].fi > lim[i].se) {
			cout << "no\n";
			return;
		} else {
			if (s[i - 1] == '1') {
				b[i - 1] = b[i] - a[i];
			} else {
				b[i - 1] = lim[i - 1].se;
			}
		}
	}

	vi ans(n + 1);
	rep(i, 1, n) {
		ans[i] = b[i] - b[i - 1];
	}
	cout << "yes\n";
	rep(i, 1, n) {
		cout << ans[i] << ' ';
	}
	cout << '\n';

}

赛时没想到这个简洁的做法,用的是另一个略复杂的做法。考虑每一段si=1s_i=1si=1的连续段,假设这一段最左侧是rootrootroot,这一段的相对起伏是完全确定的,唯一不确定的只有rootrootroot的取值,随着rootrootroot取值的改变,这一段可以上下平移。

这一段的每个点,都对rootrootroot的取值能产生一个约束,这些约束如果没有矛盾,则可以确定rootrootroot的取值。

一段内的点,对rootrootroot的约束是:

  • 如果ci<ci−1c_i\lt c_{i-1}ci<ci1,那么ci=bic_i=b_ici=bi是确定的,加上这一段都是连续的si=1s_i=1si=1,也就是aia_iai的值都是确定的,那么从rootrootrootiii,这一段的aia_iai的和sumsumsum也是确定的,所以broot=ci−sumb_{root}=c_i-sumbroot=cisum也是确定的。如果这样唯一确定的rootrootroot值有多个,则必须全都相等,否则矛盾
  • 如果ci=ci−1c_i=c_{i-1}ci=ci1,那么bi≤cib_i \le c_ibici,对rootrootroot的约束只有≤ci−sum\le c_i-sumcisum,确定了一个上界,这些上界应该同时成立,取min⁡\minmin,并且上界不能和前面唯一确定的值冲突

如果没有冲突,则有解。构造时,对于每一个连续的si=1s_i=1si=1段,只需要确定rootrootroot即可全部确定具体值,rootrootroot如果有唯一确定值,就用这个值,否则只有一个上界,直接取上界。然后根据确定的aia_iai,累加前缀和,计算出其它bib_ibi

void solve() {
	int n;
	cin >> n;
	string s;
	cin >> s;
	s = ' ' + s;

	vi a(n + 1);
	rep(i, 1, n) {
		cin >> a[i];
	}

	vi c(n + 1);
	rep(i, 1, n) {
		cin >> c[i];
	}

	rep(i, 2, n) {
		if (c[i] < c[i - 1]) {
			cout << "no\n";
			return;
		}
	}

	vi rt(n + 1), sum(n + 1);
	rep(i, 1, n) {
		if (s[i] == '1') {
			rt[i] = rt[i - 1];
			sum[i] = sum[i - 1] + a[i];
		} else {
			rt[i] = i;
		}
	}

	vi mx(n + 1, inf);
	vi val(n + 1, inf);
	val[0] = mx[0] = 0;

	rep(i, 1, n) {
		int f = rt[i];
		mx[f] = min(mx[f], c[i] - sum[i]);

		if (i == 1 || c[i] > c[i - 1]) {
			if (val[f] != inf && val[f] != c[i] - sum[i]) {
				cout << "no\n";
				return;
			} else {
				val[f] = c[i] - sum[i];
			}
		}
	}

	vi b(n + 1);
	rep(i, 0, n) {
		if (rt[i] == i) {
			if (val[i] != inf) {
				if (val[i] > mx[i]) {
					cout << "no\n";
					return;
				} else {
					b[i] = val[i];
				}
			} else {
				b[i] = mx[i];
			}
		}
	}

	rep(i, 1, n) {
		b[i] = b[rt[i]] + sum[i];
	}

	vi ans(n + 1);
	rep(i, 1, n) {
		ans[i] = b[i] - b[i - 1];
	}

	cout << "yes\n";
	rep(i, 1, n) {
		cout << ans[i] << ' ';
	}
	cout << '\n';



}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐