Educational Codeforces Round 76 (Rated for Div. 2)
A - Two Rival Students
题意:共n个人排一排,两个人,位于a,b,相邻位置交换至多x次,最大化abs(a-b)的值。
题解:每次交换至多+1,不能超过n-1。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int t;
scanf("%d", &t);
while(t--) {
int n, x, a, b;
scanf("%d%d%d%d", &n, &x, &a, &b);
printf("%d\n", min(n - 1, abs(a - b) + x));
}
return 0;
}
B - Magic Stick
题意:给一个数a,有无限次的两种操作:1、若a是偶数,则加上a的一半。2、若a>1,则减去1。问是否可以从x构造出y。
题解:有无限次减法,就看1操作能到达哪里就可以。首先1是不能动的,y<=1。其次,2可以到3,3可以到2,没办法突破到4,所以x=2或x=3时,y<=3。而4可以有4->6->9->8->12->18->27->26->39->...这样无限做下去,所以4以上必有解。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int t;
scanf("%d", &t);
while(t--) {
ll x, y;
scanf("%lld%lld", &x, &y);
bool suc = 0;
if(x == 1)
suc = (y <= 1);
else if(x <= 3)
suc = (y <= 3);
else
suc = 1;
puts(suc ? "YES" : "NO");
}
return 0;
}
C - Dominated Subarray
题意:n(n<=2e5)个数的序列a,ai<=n,找最短的“被支配子区间”,一个子区间被支配有两个条件:1、长度>=2,2、有唯一的众数。
题解:一开始还想二分,二分长度然后验证,还写了一个验证的算法,可以均摊O(1)转移出众数(每次尺取的时候,众数至多变化1)。但是转头一想,小的区间不行大的未必不行的。比如我构造一个这样的:
1
7
2 2 4 4 2 2 4
假如一开始二分到4,则验证失败,任意一个子区间的众数都是2和4两个。但最短子区间长度是2。
后来想了一下,是不是最开始考虑的,是否是最短的两个相邻相同元素的差呢?简单证明之后发现的确是这样。
假如最短的两个相邻相同元素为下面的样子:
x,y1,y2,y3,...,yk,x
那么yi互不相等,也当然和x不相等,否则有更短的yi,yj与假设矛盾。则这个区间一定是“被支配子区间”。
但为什么最短“被支配子区间”一定是这样呢?
从x往两边扩展,长度肯定不会更短,而往内收缩,则不会有更短的“被支配子区间”,所以这样的相邻x组成的为被支配子区间的构成元素,长的可以由若干个小的组合得到。在这个算法下n=1不需要特判。注意别被卡memset。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, a[200005];
int pre[200005];
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int t;
scanf("%d", &t);
while(t--) {
scanf("%d", &n);
memset(pre, -1, sizeof(pre[0]) * (n + 1));
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
int minlen = 1e9;
for(int i = 1; i <= n; ++i) {
if(pre[a[i]] != -1)
minlen = min(minlen, i - pre[a[i]] + 1);
pre[a[i]] = i;
}
if(minlen == 1e9)
minlen = -1;
printf("%d\n", solve());
}
return 0;
}
但是貌似t只有1000,在cf是卡不了memset的样子。
二分的错误算法,不过算是知道怎么O(1)转移众数了。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, a[200005];
int occ[200005];
int maxi[200005];
bool check(int len) {
memset(occ, 0, sizeof(occ[0]) * (n + 1));
memset(maxi, 0, sizeof(maxi[0]) * (n + 1));
int maxtop = 0;
for(int i = 1; i <= len; ++i) {
--maxi[occ[a[i]]];
++occ[a[i]];
++maxi[occ[a[i]]];
maxtop = max(maxtop, occ[a[i]]);
}
if(maxi[maxtop] == 1)
return 1;
else {
for(int i = len + 1; i <= n; ++i) {
--maxi[occ[a[i]]];
++occ[a[i]];
++maxi[occ[a[i]]];
--maxi[occ[a[i - len]]];
--occ[a[i - len]];
++maxi[occ[a[i - len]]];
while(maxi[maxtop] == 0)
--maxtop;
maxtop = max(maxtop, occ[a[i]]);
if(maxi[maxtop] == 1)
return 1;
}
}
return 0;
}
int solve() {
int l = 2, r = n;
//printf("l=%d r=%d\n", l, r);
while(1) {
int m = l + r >> 1;
printf("m=%d\n", m);
if(l == m) {
if(check(l))
return l;
else if(check(r))
return r;
else
return -1;
}
if(check(m))
r = m;
else
l = m + 1;
}
}
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int t;
scanf("%d", &t);
while(t--) {
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
if(n == 1) {
puts("-1");
continue;
}
printf("%d\n", solve());
}
return 0;
}
D - Yet Another Monster Killing Problem
题意:有n个怪物,从左到右刷,每个怪攻击力ai,你有m个英雄,每个英雄有攻击力pi和耐力si。每天你放一个英雄往前刷,直到耐力消耗完/怪刷完/遇到攻击比他高的怪,他就会回城。英雄可以按任意顺序用任意次,无解输出-1。
题解:一开始想的是,肯定是个偏序关系,有用的英雄要么攻击高,要么耐力高,组成一条折线(一开始还在想凸壳),然后每一天就贪心尽可能往前走,由于英雄被筛选过,所以往前走d步要找的是si>=d的第一个英雄。找不到就-1。找得到就比较两者攻击力,攻击力不够也是-1,这样貌似变成一个RMQ问题。
所以排序筛选之后贪心,二分往前走的值,然后用ST表RMQ,是严格的O(nlogn)。
注意ST表的访问不能够越界!
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Hero {
int s, p;
bool operator<(const Hero &h)const {
return s != h.s ? s < h.s : p < h.p;
}
} h[200005], H[200005];
int n, hn, a[200005];
int st[200005], stop;
int id[200005];
//耐力超过i的攻击力最高的英雄的编号为id[i];
int cur;
class ST_Table {
private:
static const int MAXLOGN = 19;
static const int MAXN = 200005;
int n, logn[MAXN + 5];
int f[MAXN + 5][MAXLOGN + 1];
public:
void init1() {
logn[1] = 0;
for(int i = 2; i <= MAXN; i++)
logn[i] = logn[i >> 1] + 1;
}
void init2(int _n) {
n = _n;
for(int i = 1; i <= n; i++)
f[i][0] = a[i];
for(int j = 1, maxlogn = logn[n]; j <= maxlogn; j++) {
for(int i = 1; i + (1 << j) - 1 <= n; i++)
f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
}
int query(int l, int r) {
// l = max(1, l), r = min(n, r);
// if(l > r)
// exit(-1);
int s = logn[r - l + 1];
return max(f[l][s], f[r - (1 << s) + 1][s]);
}
} ST;
bool check(int len) {
int p = id[len];
if(p == -1)
return 0;
else
return H[p].p >= ST.query(cur + 1, min(n, cur + len));
}
int solve() {
int l = 1, r = H[hn].s;
while(1) {
int m = l + r >> 1;
if(l == m) {
if(check(r))
return r;
else if(check(l))
return l;
else
return -1;
}
if(check(m))
l = m;
else
r = m - 1;
}
}
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
ST.init1();
int t;
scanf("%d", &t);
while(t--) {
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
ST.init2(n);
scanf("%d", &hn);
for(int i = 1; i <= hn; ++i) {
scanf("%d%d", &h[i].p, &h[i].s);
}
sort(h + 1, h + 1 + hn);
stop = 0;
for(int i = 1; i <= hn; ++i) {
while(stop && h[i].p >= h[st[stop]].p)
--stop;
st[++stop] = i;
}
hn = stop;
for(int i = 1; i <= hn; ++i)
H[i] = h[st[i]];
for(int i = 1, curid = 1; i <= n; ++i) {
while(curid <= hn && H[curid].s < i)
++curid;
if(H[curid].s >= i)
id[i] = curid;
else
id[i] = -1;
}
cur = 0;
int ans = 0;
while(cur < n) {
int d = solve();
if(d == -1) {
ans = -1;
break;
} else {
cur += d;
++ans;
}
}
printf("%d\n", ans);
}
return 0;
}
注1:要筛选出一个有用的偏序集合的时候,貌似是排序然后单调栈是最好理解的。在这题中,因为维度s的取值范围足够小,可以不进行排序而使用dp来进行转移,具体做法就是在每个耐力值上打上最大的pi标记,然后从n向1传递pi,这样形成的就是一个偏序集合,复杂度O(n+m)。
注2:把上面这个优化掉之后,还有没有可以进一步降低复杂度的方法呢?注意到二分长度然后贪心的时候实际上直接往右走然后不断记录这一次取得的RMQ的值就可以了,走到不能再走就开始新的一天,这样ST表和二分的复杂度都省了。总复杂度O(n)。
但是跑的时间反而更长了,迷惑。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, m, a[200005];
int p[200005];
//p[i]表示s超过i的最高攻击力
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int t;
scanf("%d", &t);
while(t--) {
scanf("%d", &n);
memset(p, -1, sizeof(p[0]) * (n + 1));
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
scanf("%d", &m);
for(int i = 1, pi, si; i <= m; ++i) {
scanf("%d%d", &pi, &si);
p[si] = max(p[si], pi);
}
for(int i = n - 1; i >= 1; --i)
p[i] = max(p[i], p[i + 1]);
int i = 0, j = 0, rmq = -1, ans = 1;
while(j < n) {
++j;
rmq = max(rmq, a[j]);
if(p[j - i] < rmq) {
if(rmq > p[1]) {
ans = -1;
break;
} else {
i = j - 1;
++ans;
rmq = a[j];
}
}
}
printf("%d\n", ans);
}
return 0;
}
标签:dp,贪心
E - The Contest
题意:有n道题,3个程序员,程序员A从前往后开题,程序员C从后往前开题,程序员B做剩下的。但是这一次题目印刷有问题,他们要分配这个题目,每次分配可以给出自己手中的一份题目去其他程序员手中。求最小的分配次数。
补题思路:看起来应该从程序员A和程序员C入手,首先已经从两端连续的一部分直接保持不变。然后考虑第一个断点,有两种处理方式:1、从其他程序员处填充到这个断点,2、把这个断点连同后面所有的都给其他程序员。显然AC都选择2之后一定是满足题意的了。所以这个题目是不是就要要判断每一步是走1还是走2呢?从简单的情形开始想,假如断点只断了1个,不妨修复断点,这样并不会更差。这样就卡住了。
但是假如从后往前决策的话呢?扫到一个数,则操作2的成本+1,扫到一个空格,则操作1的成本+1?设dp[i]为解决程序员A手中的数字i及其之后的所有数字所需的最小代价,对于最后一个断点,dp[i]=0,因为可以通过在前面连续来使得它不需要任何花费。一直转移到第一个空格位置,在空格i处,连接成本+1,丢弃成本为后面数字的数量,处理的最小成本为1。
所以是不是倒着dp就完事了?
把A处理好之后,还有可能有B和C之间的问题,这个看起来比较难解决。
这个是错的代码,没有考虑A对C的影响。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[200005], b[200005], c[200005];
int dp[200005];
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int A, B, C;
scanf("%d%d%d", &A, &B, &C);
for(int i = 1; i <= A; ++i)
scanf("%d", &a[i]);
sort(a + 1, a + 1 + A, greater<int>());
for(int i = 1; i <= B; ++i)
scanf("%d", &b[i]);
for(int i = 1; i <= C; ++i)
scanf("%d", &c[i]);
sort(c + 1, c + 1 + C);
int ans = 0;
if(A) {
int cnt1 = 0, cnt2 = 0;
for(int i = 1; i <= A; ++i) {
cnt2 += 1;
cnt1 += (i > 1) ? (a[i - 1] - a[i] - 1) : 0;
dp[i] = dp[i - 1] + min(cnt1, cnt2);
}
ans += dp[A];
}
if(C) {
int cnt1 = 0, cnt2 = 0;
for(int i = 1; i <= C; ++i) {
cnt2 += 1;
cnt1 += (i > 1) ? (c[i] - c[i - 1] - 1) : 0;
dp[i] = dp[i - 1] + min(cnt1, cnt2);
}
ans += dp[C];
}
printf("%d\n", ans);
return 0;
}
换一种思路,假如每个数字记录自己现在属于某个程序员,这样是不是可以转移呢?设dp[i][1]为前i个数字全部属于A的最小方案,dp[i][2]是从第i个数字开始属于B的最小方案,dp[i][3]是从第i个数字开始全部属于C的最小方案。
所以为什么要放在E题呢?可能对于dp的掌握是足够的,下次多给一点信心和时间(C不要乱写二分,要先确认正确性,D想到排序筛选之后,以后就知道可以用单调栈筛选,或者直接把这个dp出来,RMQ的使用下一次也会更快,甚至也可以观察出不需要二分直接往前贪心就可以)吧。错过了一次涨一大波分的机会。不过实际上做D花费了很多时间,E的难度虽然不高但是限时的情况下很容易崩溃。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int id[200005], dp[200005][4];
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int A, B, C;
scanf("%d%d%d", &A, &B, &C);
for(int i = 1, ai; i <= A; ++i) {
scanf("%d", &ai);
id[ai] = 1;
}
for(int i = 1, bi; i <= B; ++i) {
scanf("%d", &bi);
id[bi] = 2;
}
for(int i = 1, ci; i <= C; ++i) {
scanf("%d", &ci);
id[ci] = 3;
}
int n = A + B + C;
for(int i = 1; i <= n; ++i) {
if(id[i] == 1) {
dp[i][1] = dp[i - 1][1];
dp[i][2] = min(dp[i - 1][1], dp[i - 1][2]) + 1;
dp[i][3] = min(dp[i - 1][1], min(dp[i - 1][2], dp[i - 1][3])) + 1;
} else if(id[i] == 2) {
dp[i][1] = dp[i - 1][1] + 1;
dp[i][2] = min(dp[i - 1][1], dp[i - 1][2]);
dp[i][3] = min(dp[i - 1][1], min(dp[i - 1][2], dp[i - 1][3])) + 1;
} else {
dp[i][1] = dp[i - 1][1] + 1;
dp[i][2] = min(dp[i - 1][1], dp[i - 1][2]) + 1;
dp[i][3] = min(dp[i - 1][1], min(dp[i - 1][2], dp[i - 1][3]));
}
}
printf("%d\n", min(dp[n][1], min(dp[n][2], dp[n][3])));
return 0;
}
但是这个E是有data structure的标签的,找一下data structure的思路?
F - Make Them Similar
题意:两个数字相似,当且仅当它的二进制位1数量相同,给n(<=100)个数字ai(<2^30),问一个x使得所有的ai异或x之后都相似。无解输出-1。
题解:一种最暴力的想法是直接枚举x然后暴力统计是否合法,这样枚举x是不可能的,但是提供一个思路。折半,分成高15位和低15位分别枚举(好像和最后一次大号上分的思路有点像)!
首先处理高15位,然后求出差值数组(也就是每个数相对第一个数的1的个数的差值,把这个差值编码成vector之后暴力存进map里面(也就是可以根据差值来查找x的高15位))。
然后处理低15位,也是求出差值数组,把差值数组取反之后在map中查询(比如我现在低15位a2比a1多1个1,我就找高位a2比a1少1个1的),查找成功立刻保存退出。使用map只是常数略大,理论复杂度比Trie还快的,因为map是15层(不过单次比较并应该都是从头开始比较),Trie是100层。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[105];
map<vector<int>, int> m;
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
vector<int> vec(n);
const int BIT = 15;
//处理高15位,并存表
for(int x = 0; x < (1 << BIT); ++x) {
for(int i = 1; i <= n; ++i)
vec[i - 1] = __builtin_popcount((a[i] >> BIT)^x);
for(int i = n; i >= 1; --i)
vec[i - 1] -= vec[0];
m[vec] = x;
}
//处理低15位,并查表
const int BITMASK = (1 << BIT) - 1;
for(int x = 0; x < (1 << BIT); ++x) {
for(int i = 1; i <= n; ++i)
vec[i - 1] = __builtin_popcount((a[i] & BITMASK)^x);
for(int i = n; i >= 1; --i)
vec[i - 1] = vec[0] - vec[i - 1];
auto it = m.find(vec);
if(it != m.end()) {
printf("%d", ((it->second) << BIT) | x);
return 0;
}
}
puts("-1");
return 0;
}
启发:有时候要换个角度,不要从n与ai之间的关系入手,可以从值域入手,对于这种折一半之后很好做的,可以试试看折半查找。只不过这个是对值域折一半,下次应该获得“直觉提升”buff。
注:
1、不要把按位与写成逻辑与!
2、提示我可以使用一个Trie,每一层是固定的31个节点[-15,15]。要把这个Trie改成每层使用map的感觉也很简单,就修改一个TrieNode就可以了。
可能可以的奇怪的解法:设想一种搞笑的算法,类似遗传算法的思路,先随机生成10000个值域范围内的数,然后与ai都异或,计算他们的方差,选方差最小的300个作为种子,每个种子改变其中的1位又有9000多个数,再计算他们的方差……遗传30次之后结果会是什么呢?
标签:位运算,折半枚举
G - Divisor Set
一道分治NTT的题目,当然也可以用分治FFT来做。
题意:一个大数x,给出它的质因数分解式。设x的正因数的集合为S,在S中选出最大的子集T,使得T中任意一对元素都没有因数/倍数关系。求T的大小 mod 998244353。
没有题解:非常显然这个是和质因数的大小本身没有关系的,只和x含有的质因数的幂次有关系。从生成函数的角度看,就是选出一堆质因数的幂次的向量,使得没有任何一个向量每个分量都>=另一个向量的每个分量。非常显然,假如我们选多项式的某一项的系数作为T,那么这个是满足题意的。假如只能选择多项式的系数,那么是选择中间的那项的系数最大(非常直观,但怎么证明?)。那为什么一定要选择同一个项的系数,而不能把几个项的系数混在一起呢?并不知道怎么证明,等题解。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[200005], b[200005], btop;
const int MAXN = 1e6, MAXLOGN = 20, mod = 998244353;
int add_mod(int x, int y) {
x += y;
if(x >= mod)
x -= mod;
return x;
}
int sub_mod(int x, int y) {
x -= y;
if(x < 0)
x += mod;
return x;
}
ll mul_mod(ll x, int y) {
x *= y;
if(x >= mod)
x %= mod;
return x;
}
int pow_mod(ll x, int n) {
ll res = 1;
while(n) {
if(n & 1)
res = mul_mod(res, x);
x = mul_mod(x, x);
n >>= 1;
}
return res;
}
int gl[MAXLOGN + 1];
void init() {
for(int len = 2; len <= MAXN; len <<= 1)
gl[__builtin_ctz(len)] = pow_mod(3, (mod - 1) / len);
}
void NTT(int a[], int n, int op) {
for(int i = 1, j = n >> 1; i < n - 1; ++i) {
if(i < j)
swap(a[i], a[j]);
int k = n >> 1;
while(k <= j) {
j -= k;
k >>= 1;
}
j += k;
}
for(int len = 2; len <= n; len <<= 1) {
int g = gl[__builtin_ctz(len)];
for(int i = 0; i < n; i += len) {
int w = 1;
for(int j = i; j < i + (len >> 1); ++j) {
int u = a[j], t = mul_mod(a[j + (len >> 1)], w);
a[j] = add_mod(u, t), a[j + (len >> 1)] = sub_mod(u, t);
w = mul_mod(w, g);
}
}
}
if(op == -1) {
reverse(a + 1, a + n);
int inv = pow_mod(n, mod - 2);
for(int i = 0; i < n; ++i)
a[i] = mul_mod(a[i], inv);
}
}
int A[MAXN + 5], B[MAXN + 5];
int pow2(int x) {
int res = 1;
while(res < x)
res <<= 1;
return res;
}
void convolution(int A[], int B[], int Asize, int Bsize) {
int n = pow2(Asize + Bsize - 1);
memset(A + Asize, 0, sizeof(A[0]) * (n - Asize));
memset(B + Bsize, 0, sizeof(B[0]) * (n - Bsize));
NTT(A, n, 1);
NTT(B, n, 1);
for(int i = 0; i < n; ++i)
A[i] = mul_mod(A[i], B[i]);
NTT(A, n, -1);
return;
}
vector<int> vec[200005], evec;
struct PriorityQueueNode {
int siz, id;
bool operator<(const PriorityQueueNode &pqn) const {
return siz > pqn.siz;
}
} pqn;
priority_queue<PriorityQueueNode> pq;
void solve() {
//哈夫曼分治
init();
while(pq.size() > 1) {
int Aid = pq.top().id, Asize = vec[Aid].size();
for(int i = 0; i < Asize; ++i)
A[i] = vec[Aid][i];
pq.pop();
int Bid = pq.top().id, Bsize = vec[Bid].size();
for(int i = 0; i < Bsize; ++i)
B[i] = vec[Bid][i];
pq.pop();
convolution(A, B, Asize, Bsize);
Asize = Asize + Bsize - 1;
vec[Aid].resize(Asize);
for(int i = 0; i < Asize; ++i)
vec[Aid][i] = A[i];
pq.push({Asize, Aid});
vec[Bid] = evec;
}
}
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
sort(a + 1, a + 1 + n);
btop = 0;
for(int i = 1; i <= n; ++i) {
if(a[i] != a[i - 1])
b[++btop] = 1;
else
++b[btop];
}
sort(b + 1, b + 1 + btop);
for(int i = 1; i <= btop; ++i) {
while(vec[0].size() < b[i] + 1)
vec[0].push_back(1);
vec[i] = vec[0];
pq.push({vec[i].size(), i});
}
solve();
printf("%d\n", vec[pq.top().id][pq.top().siz >> 1]);
return 0;
}
upd1:偏序关系可以画出(化简的,也就是没有多余边的)哈斯图。哈斯图是分层的,覆盖关系连出有向边。画多几个(打表)发现哈斯图上下对称且中间是比较宽的。已知同一层的节点一定满足题意,但是为什么一定只选择同一层的呢。(题解说用OEIS,真就打表暴力啊)
参考资料
[Educational Codeforces Round 76 Editorial - Codeforces]https://codeforces.com/blog/entry/71434