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';
}