ABC459 贪心构造|树形DP|组合数学|贪心|单调栈|势能|前缀和

D

贪心 构造

给一个字符串,问能否重排,使得不存在相邻字符相同?

假设长度nnn,那么最多存在(n+1)/2(n+1)/2(n+1)/2,也就是除二上取整,这么多个位置是互不相邻的,所以相同元素最多这么多,再多,就必然有元素相邻。

所以出现次数最多的元素,如果不超过这个上界,则有解。否则无解。

构造时,把元素根据出现次数降序排序,然后先依次放在1,3,5...1,3,5...1,3,5...奇数位上,奇数位放满了,再回来放2,4,6...2,4,6...2,4,6...偶数位。这样做的话,奇数位实际上就是前面说的(n+1)/2(n+1)/2(n+1)/2个不相邻位置,由于已经保证了没有元素出现次数超过这个上界,要么奇数位能容纳下相同元素的所有出现,使其不相邻,要么奇数位放不下了,折回来放偶数位,但偶数位也要从头开始,同一种元素回不到他开始的奇数位附近,也就不会出现相邻相等。

说的有点抽象,看代码吧,总之典

c 复制代码
void solve() {
	string s;
	cin >> s;
	vi cnt(26);
	for (char c : s) {
		cnt[c - 'a']++;
	}

	int mx = 0;
	vvi a;
	rep(i, 0, 25) {
		mx = max(mx, cnt[i]);
		if (cnt[i]) {
			a.push_back({i, cnt[i]});
		}
	}

	sort(a.begin(), a.end(), [](vi & x, vi & y) {
		return x[1] > y[1];
	});

	int n = s.size();
	if (mx > (n + 1) / 2) {
		cout << "No\n";
		return;
	}
	cout << "Yes\n";

	string ans(n, '0');

	int m = a.size();


	for (int i = 0, j = 0; j < m; i = (i + 2 < n) ? i + 2 : 1 ) {
		ans[i] = (char)(a[j][0] + 'a');
		if (--a[j][1] == 0) {
			j++;
		}
	}

	cout << ans << '\n';
}

E

树形dp 组合数学

每个点有cic_ici个元素,所有元素都是互异的。每个点iii有个任务,在iii子树内取did_idi个元素。问满足所有任务的方案数?

对于一个点的任务,由于它的每个子树也都有要完成的任务,他能取的只有,每个子树完成各自任务后剩余的元素,树形dpdpdp维护一下每个点,满足所有子树的任务后,留给这个点还剩多少个元素,如果剩余不足did_idi则无解,方案数000,否则这个任务的方案数就是从剩余元素中选择did_idi个元素。每个点的方案互相独立,服从乘法原理

注意由于cic_ici很大,不能O(n)O(n)O(n)预处理O(1)O(1)O(1)计算组合数,需要用O(k)O(k)O(k)来暴力计算C(n,k)C(n,k)C(n,k)。这个过程中,由于nnn表示这个点可用的元素,是整个子树剩余累加的,大小是O(n∗ci)O(n*c_i)O(n∗ci)级别的,可能有过1e5∗1e9=1e141e5*1e9=1e141e5∗1e9=1e14,再在计算时乘上k=1e5k=1e5k=1e5,会超出long long的有效范围,所以乘的时候记得取模。注意不能在外面C(nmod  M,k)C(n \mod M ,k)C(nmodM,k)这样取模,这是不等价的

c 复制代码
int C(int n, int k) {
	int res = 1;
	for (int i = n; i >= n - k + 1; i--) {
		res = res * (i % M2) % M2;
	}
	int t = 1;
	for (int i = k; i >= 1; i--) {
		t = t * i % M2;
	}
	return res * inv(t, M2) % M2;
}
void solve() {
	int n;
	cin >> n;

	vvi g(n + 1);
	rep(i, 2, n) {
		int p;
		cin >> p;
		g[p].push_back(i);
	}

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

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

	int ans = 1;
	auto &&dfs = [&](auto &&dfs, int u, int fa)->void{
		for (int v : g[u]) {
			if (v == fa)continue;
			dfs(dfs, v, u);
			c[u] += c[v];
		}
		if (c[u] < d[u]) {
			cout << 0 << '\n';
			exit(0);
		}
		ans = ans * C(c[u], d[u]) % M2;
		c[u] -= d[u];
	};

	dfs(dfs, 1, 0);
	cout << ans;
}

F

贪心 单调栈 前缀和/带权和 非严格单调转化

每次可以aia_iai减一,ai+1a_{i+1}ai+1加一,问最少多少次操作,可以把数组变成严格单增?

首先注意到,如果我们能求出一个最优的结束状态,满足严格单增,其实可以O(n)O(n)O(n)求出需要进行的操作次数。

这有至少两种求法:

  • 第一种是计算∑i=1npreit−preis\sum_{i=1}^n pre^t_i-pre^s_i∑i=1npreit−preis,也就是初始和结束状态的所有位置前缀和做差,求和。这是对的,可以考虑物理意义:对于一个前缀[1,i][1,i][1,i],如果最终状态前缀和和开始不相等了,说明前缀中有元素跨过[i,i+1][i,i+1][i,i+1]去后面了。前缀和做差就是这个前缀的贡献。
  • 第二种是,考虑一个把一个aia_iai的111移动到aja_jaj,代价很简单,j−ij-ij−i,这是只移动111,如果移动xxx呢,代价是x∗(j−i)x*(j-i)x∗(j−i)。考虑类似势能的思想,设iii位置的势能是i∗aii*a_ii∗ai,那么如果aia_iai移动xxx到了aja_jaj。势能变化量为
    (j∗(aj+x)−i∗(ai−x))−(j∗aj−i∗ai)=x∗(j−i)(j*(a_j+x)-i*(a_i-x))-(j*a_j-i*a_i)=x*(j-i)(j∗(aj+x)−i∗(ai−x))−(j∗aj−i∗ai)=x∗(j−i),也是对的。这里的设计思想是,都是值乘上位置,发现代价里包含i,ji,ji,j项,且形式相同,考虑把这个形式就作为势能。

现在如果知道最终状态,可以用这两个做法求出代价,都是O(n)O(n)O(n)的。关键就剩下如何求最终状态了。

这也有两种做法:

做法1:

实际上可以贪心,考虑已经排好序的一段,如果后面来个新的元素,不满足升序,怎么办?需要把前面这一段,移动一部分到后面,使得合起来升序。如果是一段升序,后面又来了一段,也是同理。

由于我们只关心最终状态,可以不用模拟合并过程,直接去求合并后,代价最小的升序状态是什么样的。显然应该是一个公差为1的等差数列,x,x+1...x+len−1x,x+1...x+len-1x,x+1...x+len−1,如果还剩下一点余数rrr,不足lenlenlen的话,把余数均匀加在这一段的后缀上,也就是最后rrr个元素加一。

于是可以考虑维护一个栈,保存所有构造好的升序区间,每个区间需要记录必要信息,才能合并,至少要记录区间长度和区间元素和。由于我们知道模式,知道这两个信息,就能求出每一项了。

每次新增一个元素,把这个元素视为长度1的一个区间,检查它和栈顶的区间,直接拼接是否满足升序,如果直接满足了就直接拼接,也就是直接入栈,如果不严格升序,则和栈顶合并,变成一个严格升序区间,此时由于合并可能把区间首项变小了,又可能和栈顶第二个区间不满足升序了,在栈上递归合并。类似单调栈的过程。

最后整个序列处理完了,取出栈内元素,每个元素对应序列一个区间,还原整个序列,就是变成升序,且满足代价最小的末态,用前面提到两种方法,和初始状态计算操作次数。

实现细节上,已知长度LLL,元素和SSS,如何求一个区间的首项和末项?模式是:一个公差为1的等差数列,可能还有不足lenlenlen的余数加在后缀上

于是首项是
x=⌊S−L(L−1)2L⌋x = \left\lfloor \frac{S - \frac{L(L-1)}{2}}{L} \right\rfloorx=⌊LS−2L(L−1)⌋

余数是R=(S−L(L−1)2) mod LR = (S - \frac{L(L-1)}{2}) \bmod LR=(S−2L(L−1))modL

末项是end_val=x+L−1+(R>0?1:0)\text{end\_val} = x + L - 1 + (R > 0 ? 1 : 0)end_val=x+L−1+(R>0?1:0)

另一细节是:可能出现负数除法,负数取余,而c的默认除法是向0取证,而我们要的是向下取整,所以如果有负数,需要手动调整结果变成向下取整。也就是这里

c 复制代码
	if (nr < 0) {
		nx--;
		nr += nlen;
	}
c 复制代码
struct T {
	int x, len, sum, r;
};
void solve() {
	int n;
	cin >> n;

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

	seg.push_back({a[1], 1, a[1], 0});
	rep(i, 2, n) {
		T cur = {a[i], 1, a[i], 0};

		while (seg.size()) {
			auto t = seg.back();
			int x = t.x;
			int len = t.len;
			int r = t.r;
			int sum = t.sum;

			int lst = x + len - 1;
			if (r)lst++;
			if (lst >= cur.x) {
				seg.pop_back();
				int nsum = sum + cur.sum;

				int nlen = len + cur.len;
				int nx = (nsum - nlen * (nlen - 1) / 2) / nlen;
				int nr = (nsum - nlen * (nlen - 1) / 2) % nlen;

				if (nr < 0) {
					nx--;
					nr += nlen;
				}

				cur = {nx, nlen, nsum, nr};
			} else {
				break;
			}
		}
		seg.push_back(cur);
	}

	vi b(n + 1);

	int cur = 1;
	for (auto &t : seg) {
		int l = cur, r = cur + t.len - 1;
		for (int i = l, j = 0; i <= r; i++, j++) {
			b[i] = t.x + j;
		}

		for (int i = r, j = 1; j <= t.r; j++, i--) {
			b[i]++;
		}

		cur = r + 1;
	}

	int ans = 0;
	rep(i, 1, n) {
//		cout << b[i] << ' ';
		ans += (b[i] - a[i]) * i;
	}

	cout << ans << '\n';
}

做法2

上一个做法有地方可以优化,就是计算首项末项的式子有点麻烦,这是因为要求严格递增,于是每个区间的模式都是公差1等差数列的,加上一段1。

公差1的等差数列有个经典转换,初始把元素改成ai′=ai−ia_i'=a_i-iai′=ai−i,那么公差1等差数列,就变成一段相等了,或者说公差0的等差数列。

两个区间合并,目标也变成了合并后非严格单增,可以相等。于是合并后模式是:一段相等的元素,可能有不足lenlenlen的余数,还是变成一段1加在后缀上。

这样合并计算,首项末项计算就简单很多。

另外,对于上个做法的负数除法,一个处理是,注意到这题的结果之和状态改变量有关,所以我们开始把整个数组加上一个常数CCC不影响结果,那么加上一个足够大的数,使得把前面的数移动到后面,也不会让前面出现负数。

另外这个做法的代码,计算代价用的是前缀和,前一个代码用的是带权和。

c 复制代码
struct T {
	int len, sum;
};
void solve() {
	int n;
	cin >> n;

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

		a[i] += 1e9;
	}
	vector<T>seg;

	rep(i, 1, n) {
		T cur = {1, a[i]};

		while (seg.size()) {
			auto t = seg.back();
			int len = t.len;
			int sum = t.sum;

			int r = t.sum % t.len;
			int x = t.sum / t.len;

			int lst = x;
			if (r)lst++;
			if (lst > cur.sum / cur.len) {
				seg.pop_back();
				int nsum = sum + cur.sum;

				int nlen = len + cur.len;

				cur = {nlen, nsum};
			} else {
				break;
			}
		}
		seg.push_back(cur);
	}

	vi b(n + 1);

	int cur = 1;
	for (auto &t : seg) {
		int l = cur, r = cur + t.len - 1;
		for (int i = l; i <= r; i++) {
			b[i] = t.sum / t.len;
		}

		for (int i = r, j = 1; j <= t.sum % t.len; j++, i--) {
			b[i]++;
		}

		cur = r + 1;
	}

	int ans = 0;
	int prea = 0, preb = 0;
	rep(i, 1, n) {
//		cout << b[i] << ' ';
		prea += a[i];
		preb += b[i];
		ans += prea - preb;
	}

	cout << ans << '\n';
}
相关推荐
灰灰勇闯IT3 小时前
DeepEP:MoE 推理的 AllToAll 通信瓶颈怎么解
算法·cann
一行代码一行诗++3 小时前
goto语句
java·开发语言·算法
汉克老师3 小时前
GESP5级C++考试语法知识(十七、二分算法提高篇(二))
c++·算法·二分算法·gesp5级·gesp五级·二分算法易错点
叶小鸡3 小时前
小鸡玩算法-力扣HOT100-动态规划(下)
算法·leetcode·动态规划
信奥胡老师4 小时前
B3968 [GESP202403 五级] 成绩排序
数据结构·算法
Hwang2524 小时前
Attention 机制 02 - Add&Norm 残差机制
算法
东风破_4 小时前
LeetCode 209 · 滑动窗口经典题型
算法
计算机安禾4 小时前
【c++面向对象编程】第48篇:Lambda表达式与std::function:OOP中的函数式编程
java·c++·算法
手写码匠5 小时前
【实战评测】华为云 MaaS 平台 DeepSeek 大模型推理服务 + Dify 一键部署全攻略
人工智能·深度学习·算法·aigc