Codeforces Round #631 (Div. 2) - Thanks, Denis aramis Shitov!
题目链接:https://codeforces.com/contest/1330
A - Dreamoon and Ranking Collection
随便弄弄。
B - Dreamoon Likes Permutations
题意:给一串n个数字,范围在[1,n-1],求有多少种方法,把它断成前后两段排列。
题解:要断成两段排列,首先必须要同一个数字最多出现2次。在满足这个条件的情况下,某段i个数字的前缀形成一个排列的充要条件是:1、i个数字互不重复;2、最大值是i。这样搞一搞前缀和后缀,然后数一数前缀和后缀同时成立的方案。
C - Dreamoon Likes Permutations
题意:给 \(m\) 个长度线段A,第 \(i\) 条线段长度为 \(li\) ,要求把这些线段A按 \([1,m]\) 的顺序依次覆盖在线段B \([1,n]\) 上(不能左右越界),后面覆盖的线段A会把前面的线段A遮住。要求整条线段B \([1,n]\) 的每段都至少被一条线段A覆盖,且每条线段A至少露出1段(没有被其他线段A完全遮住)。
题解:一开始想了很多奇奇怪怪的构造,但是觉得很不自然。而且这个长度也没有什么升降序的规律。然后想到了一种容易想到的不可构造情形:线段A的总长度不够
\(n\) 。然后贪心的方法是:每条线段都依次露出最左的一小段,最后会覆盖住线段B的左半侧,假如还没有把线段B覆盖完,那么把最后一条线段A平移到最右侧,这样既不会对前面的线段造成更多的遮挡,也不会浪费长度,移动完之后可以把最后一条线段整条忽视掉,尝试把倒数第二条线段右移,直到把整条线段B都覆盖。那么是否还有一种不可构造情形是:每条线段A都露出1段之后,最长的线段A不能超过 \(m-1\) 呢?这个是错的,前面的线段A可以更长,例如第1条线段A可以长到 \(n\) ,第2条线段A可以长到 \(n-1\) ,以此类推。
int l[100005];
int ans[100005];
void TestCase() {
int n, m;
scanf("%d%d", &n, &m);
ll sum = 0;
bool suc = 1;
for(int i = 1; i <= m; ++i) {
scanf("%d", &l[i]);
sum += l[i];
if(l[i] + i - 1 > n)
suc = 0;
}
if(suc == 0 || sum < n) {
puts("-1");
return;
}
for(int i = 1; i <= m; ++i)
ans[i] = i;
int R = n + 1;
for(int i = m; i >= 2; --i) {
if(R - l[i] > ans[i - 1]) {
ans[i] = R - l[i];
R -= l[i];
} else
break;
}
for(int i = 1; i <= m; ++i)
printf("%d%c", ans[i], " \n"[i == m]);
return;
}
*D - Dreamoon Likes Sequences
一个有明显dp特征的计数题。
题意:给出两个比较大的整数 \(d,m\) ,求满足下面现在条件的数列a的数量模 \(m\) 的值,模 \(m\) 是为了不希望大家OEIS得太爽。
1、数列a至少有一个元素
2、数列a严格升序,取值范围为 \([1,d]\)
3、设数列b为:
\(\;\;\;\;\;\; b_1=a_1\)
\(\;\;\;\;\;\; b_i=b_{i-1} \oplus a_i (i \geq 2)\)
4、数列b严格升序
题解:其实仔细算出来之后发现应该还是可以OEIS得很爽,毕竟只和 \(d\) 有关。
首先观察这个奇奇怪怪的条件,异或之后要升序?那么很显然 \(a_i\) 的最高位1不能与 \(a_{i-1}\) 的最高位1相同,否则将导致 \(b_i\) 失去这个最高位。同时由于限制2, \(a_i\) 的最高位1不能低于 \(a_{i-1}\) 的最高位1,所以 \(a_i\) 的最高位1只能高于 \(a_{i-1}\) 的最高位1,且不能导致超过 \(d\) 。
那么既然选好了这个最高位1,后面的位当然就是随便选了,所以就乘上某个2的幂次。
在忽略与 \(d\) 拥有最高位1的情况时,非常好办,记 \(d\) 的最高位1位于第 \(logd\) 位,记 \(dp[i]\) 表示“最后一个数的最高位是低位的 \(i-1\) 位的种类”,特殊的记 \(dp[0]=1\) 表示“空数列 \([]\) ”也是1种。
那么 \(dp[1]=(dp[0])*1\) ,因为最高位是1的就只有1种,就是 \([1]\) 。
那么 \(dp[2]=(dp[1]+dp[0])*2\) ,因为最高位是2的,有2种,且他们都可以跟在最高位是0的或者最高位是1的后面: \([1,2],[1,3],[2],[3]\) 。
那么 \(dp[3]=(dp[2]+dp[1]+dp[0])*4\) ,因为最高位是4的,有4种,且他们都可以跟在最高位是0的或者最高位是1或者最高位是2的后面: \([1,2,4],[2,4],[3,4],[1,2,5],[2,5],[3,5],[1,2,6],[2,6],[3,6],[1,2,7],[2,7],[3,7],[4],[5],[6],[7]\) 。
麻烦事在于找类似数位dp的时候的 \(d\) 的限制。不过仔细想想没有那么复杂,首先,最高位与 \(d\) 相同,是没有选择的余地的,直接continue。否则,若 \(d\) 的这一位是0,也是没有选择的余地的,直接continue。否则, \(d\) 的这一位是1,若这一位选择填0,那么后面的选择(若还有得选)将不受限制,这个选法也是某个2的幂次。否则, \(d\) 的这一位是1,且这一位选择填1,则继续迭代下去,直接continue。这种算法统计了在 \(d\) 的某位1填了0的种类,补上和 \(d\) 完全相等的这一种,就是 \(dp[logd]\) 的最后一个元素的选择方案,用来替代前面的那些2的幂次,还是要乘上前面的 \(dp\) 值的和。
最后,把所有的 \(dp\) 值加起来。
ll dp[80];
void TestCase() {
ll d, m;
scanf("%lld%lld", &d, &m);
ll cd = d;
int logd = 0;
while(cd) {
++logd;
cd >>= 1;
}
memset(dp, 0, sizeof(dp));
dp[0] = 1;
for(int i = 1; i <= logd; ++i) {
if(i < logd) {
//最高为取1,后面i-1位任取
ll cnt = (1ll << (i - 1)) % m;
ll prefix = 0;
for(int j = i - 1; j >= 0; --j)
prefix += dp[j];
prefix %= m;
dp[i] = prefix * (1ll << (i - 1)) % m;
} else {
//选d本身,是1种选法
ll cnt = 1;
for(int j = logd - 1; j >= 0; --j) {
//最高位必定取1
if(j == logd - 1)
continue;
//不是最高位,且d这一位是1
if((1ll << j)&d)
//这一位选择0,后面的任选
cnt += (1ll << j) % m;
//这一位依然受限
}
ll prefix = 0;
for(int j = i - 1; j >= 0; --j)
prefix += dp[j];
prefix %= m;
dp[i] = prefix * cnt % m;
}
}
ll sum = 0;
for(int i = 1; i <= logd; ++i)
sum += dp[i];
sum %= m;
printf("%lld\n", sum);
}
因为担心溢出FST重交了一发,但是事实上这个并不会溢出,虽然做了很多次加法,模之前做乘法有溢出的可能,但是未必参与加法的结果有这么大(因为这个数值几乎是只与位数有关的,而且每次扩大一个很大的倍数,非常容易与5e8之类大数的擦肩而过)。
不过反正上不了橙色,不亏。
启发:注意处理空数列的情况,以及这种求<=x的满足某种条件的数的个数,可以从数位受限去想,具体的做法是:若这一位选择了比较小的数,则后面的位就会解除<=x这个条件,只受到其他条件的制约,否则就继续往下处理,最后考虑是否要补上x的情况。
*E - Drazil Likes Heap
看了这个题解觉得挺巧妙的,不过自己当时可能是没有这个勇气去做了吧。
题意:定义一种 \(h\) 阶的“奇怪堆”,堆中没有相同的元素。这个奇怪堆首先是一个大顶堆,其次是一棵高度为 \(h\) 的满二叉树(有 \(2^h-1\) 个节点的完全二叉树)。然后定义一种“奇怪堆”的“奇怪删除”,这个删除不是先把要删除的元素换到末尾,而是直接原地删除(若这个点没有儿子,则直接删除,否则用其大儿子代替之,然后递归给大儿子),所以删除结束之后一般剩下的就不再是堆了。但是题目要求给出一个删除的序列,使得删除完这些序列之后,剩下的恰好是一个 \(g\) 阶的“奇怪堆”,且这个“奇怪堆”的剩余元素总和最小。
题解:首先证明一个结论:假如删除一个点使得剩余的高度不少于 \(g\) ,则删除这个节点比删除这个节点的大儿子更优。这个其实很好想,无论是删除这个节点还是删除这个节点的大儿子,都是多出一个空位,且对树的形态的改变相同(无论是这个节点还是这个节点的大儿子占着这个位置,都会比小儿子以及小儿子的所有后代都要大),那么删除这个节点会使得总和变得更小。把某个节点到其大儿子,以及大儿子到其大孙子,以及……的链称为“大链”,容易看到上面的结论就是不断吐出“大链”的过程,注意吐到一半的时候有可能会切换到原本的小儿子变成“大链”。否则,这个节点到大儿子以及递归下去的“大链”已经是高度恰好是 \(g\) ,这个时候不可以再对“大链”进行操作,需要递归给两个儿子,让他们自己寻找自己的新的“大链”。
#define ls (2 * id)
#define rs (2 * id + 1)
int h, g;
int a[(1 << 21) + 5];
int ans[(1 << 20) + 5], atop;
int dep2(int id) {
if(a[ls] == 0 && a[rs] == 0)
return 1;
if(a[ls] > a[rs])
return dep2(ls) + 1;
else
return dep2(rs) + 1;
}
bool CheckRemove(int id, int dep) {
return dep + dep2(id) - 1 > g;
}
void Maintain(int id, bool flag = false) {
if(flag)
ans[++atop] = id;
if(a[ls] == 0 && a[rs] == 0) {
a[id] = 0;
return;
}
if(a[ls] > a[rs]) {
a[id] = a[ls];
Maintain(ls);
} else {
a[id] = a[rs];
Maintain(rs);
}
return;
}
void solve(int id, int dep) {
if(a[id] == 0)
return;
while(CheckRemove(id, dep))
Maintain(id, true);
solve(ls, dep + 1);
solve(rs, dep + 1);
return;
}
void TestCase() {
scanf("%d%d", &h, &g);
int n = (1 << h) - 1;
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
atop = 0;
solve(1, 1);
int m = (1 << g) - 1;
ll sum = 0;
for(int i = 1; i <= m; ++i)
sum += a[i];
printf("%lld\n", sum);
for(int i = 1; i <= atop; ++i)
printf("%d%c", ans[i], " \n"[i == atop]);
for(int i = 1; i <= n; ++i)
a[i] = 0;
return;
}