Codeforces Round #618 (Div. 2)
准备回到紫色了,转了一圈回来其实实力是变强了的吧?收集了一些暂时体现不出来的经验。而且比赛这种东西的确很难说的,不会就是不会。
题目链接:https://codeforces.com/contest/1300
A - Non-zero
题意:有一个(可正可负的)整数序列,每次操作把一个数++,求最少的操作使得整个数量的sum和product都不为0。
题解:首先,每个当前就是0的数至少都需要++一次,先把这个算上。然后product就一定不会为0了,此时判断sum是否为0。若为0则选任意一个不是-1的++就可以了。显然sum为0,且不是全0的,至少有一个正数一个负数,就选一个正数++。
B - Assigning to Classes
题意:给 \(2n\) 个数的集合A,分成两个size为奇数的集合S和T,使得这两个集合的中位数的差尽可能小,求这个差。
题解:二话不说就枚举。
情况1:首先只分配1个数给S,分配完之后T的中位数显然是A集合原本的两个中位数里的一个,且显然若分配了“后半”中的一个数给S,T的中位数就是左中位数。否则就是右中位数。此时肯定是随便分配左右中位数中的一个给S是最优的。
然后分配3个数给S,很显然若三个数中有至少一个来自“前半”,且至少一个来自“后半”,则化归到情况1。否则就是三个都来自同一半,这种显然是不好的。
到这里基本已经猜出结果了,答案就是左右中位数的差。
证明:假如两个集合的中位数不是左右中位数,显然他们的中位数一个比右中位数大,另一个比左中位数小,只会更差。
C - Anu Has a Function
题意:定义 \(f(x,y)=(x|y)-y\) 。安排数列的顺序,使得 \(f(...f(f(a_1,a_2),a_3)...,an)\) 最大。
题解:易知 \(f(x,y)=x\&(~y)\) 那么意思就是选一个x,然后y的每个二进制位1都会把x的对应位置0。假设一种暴力算法 \(O(n^2)\) 枚举x然后暴力跑一遍。这里有重复的信息可以利用。考虑最后贡献答案的只可能是满足下面所有条件的:
1、x的这一位是1
2、没有任何一个y的这一位是1
换言之只有x独占这个1才是答案。所以可以分解每个位统计其1的数量。
int cnt[32];
int a[200005];
void test_case() {
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
for(int i = 1; i <= n; ++i) {
int tmp = a[i];
for(int k = 0; k <= 30; ++k) {
cnt[k] += tmp & 1;
tmp >>= 1;
}
}
int maxid = -1, ans = -1;
for(int i = 1; i <= n; ++i) {
int tmp = a[i];
int cur = 0;
for(int k = 0; k <= 30; ++k) {
if((cnt[k] == 1) && (tmp & 1))
cur |= (1 << k);
tmp >>= 1;
}
if(cur > ans) {
ans = cur;
maxid = i;
}
}
swap(a[maxid], a[1]);
for(int i = 1; i <= n; ++i)
printf("%d%c", a[i], " \n"[i == n]);
}
但是事实上 \(\&\) 运算是满足交换律结合律的,可以预处理 \(~y\) 的前缀和后缀直接得到(像极了之前那个选n-1个矩形的题,也是dls一下子秒了)。或者预处理 \(y\) 的 \(|\) 前缀和后缀。
没什么意思就不再写一次了。
*D - Aerodynamic
写错下标真是尴尬,忘记了“经过n/2条边才是对边”。
题意:给一个严格凸包(也就是没有三点共线的凸包)P,现在可以把P平移,且要求原点一直在P中,其轨迹覆盖的位置的点集就是T。问P和T是否相似。
题解:显然,假如是奇数边的多边形就不相似,因为底边沿着P移动的时候上轨迹是一条线段,不可能和点相似。
那么现在至少是偶数边的多边形,画一画发现平行四边形满足而梯形不满足。仔细想想假如对边不平行就不会满足,因为原点沿着A边滑动的情况下,对边B覆盖的轨迹的凸包肯定是与A平行的,所以B也一定要和A平行。
对边分别平行的多边形不一定是的对边相等的。其实应该很显然!把底边平行下移,在极限的时候会缩短到退化成一个点。
int sgn(ll x) {
return (x == 0) ? 0 : (x > 0 ? 1 : -1);
}
struct Point {
ll x, y;
Point() {}
Point(ll x, ll y): x(x), y(y) {}
ll len2() {
return x * x + y * y;
}
friend Point operator-(const Point &a, const Point &b) {
return Point(a.x - b.x, a.y - b.y);
}
friend ll operator*(const Point &a, const Point &b) {
return a.x * b.x + a.y * b.y;
}
friend ll operator^(const Point &a, const Point &b) {
return a.x * b.y - a.y * b.x;
}
} P[200005];
struct Segment {
Point s, t;
Segment() {}
Segment(const Point &s, const Point &t): s(s), t(t) {}
//判断两条直线是否平行
bool LineParallelWithLine(const Segment &l) {
return !sgn((s - t) ^ (l.s - l.t));
}
ll len2() {
return (s - t).len2();
}
} L[200005];
int n;
int id(int x) {
if(x > n)
x -= n;
return x;
}
void test_case() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%lld%lld", &P[i].x, &P[i].y);
if(n % 2 == 1) {
puts("No");
return;
}
for(int i = 2; i <= n; ++i)
L[i] = Segment(P[i], P[i - 1]);
L[1] = Segment(P[1], P[n]);
for(int i = 1; i <= n / 2; ++i) {
if(L[i].len2() != L[id(i + n / 2)].len2()) {
puts("No");
return;
}
if(!L[i].LineParallelWithLine(L[id(i + n / 2)])) {
puts("No");
return;
}
}
puts("Yes");
return;
}
提示要准备一个ll作为坐标的几何板子。
*E - Water Balance
题意:给一个数列,每次操作可以选一段区间,然后把区间中的每个数都设为平均值。求一个字典序最小的结果。
题解:一开始想线段树,然后倍增找最长的区间,这里假在这个不满足二分性,长度为m的区间不满足不代表长度为m-1的区间不满足。显然字典序最小的结果的数列的前缀和也是字典序最小的。所以每次操作就相当于从 \(a_i=\frac{\sum\limits_{i=l}^{r}a_i}{r-l+1}=\frac{p_r-p_{l-1}}{r-l+1}\) 变成了 \(p_i=p_{l-1}+\frac{p_r-p_{l-1}}{r-l+1}\cdot(i-l+1)\) ,看上去就是一个直线的方程!或者换成 \(p_i=p_{l}+\frac{p_r-p_{l-1}}{r-l+1}\cdot(i-l)\) ,更直观。把 \((i,p_i)\) 画在数轴上,所以其实就是每次可以选择连接两个点 \(x=l,x=r\) 。由于横坐标的间隔是固定的,斜率的大小就相当于差分的大小,所以要原数组的字典序最小,就是要这个图中的一个下凸包。
int n;
ll p[1000005];
int s[1000005], top;
char ans[105];
void test_case() {
read(n);
for(int i = 1; i <= n; ++i)
read(p[i]);
if(n == 1) {
printf("%.9f\n", (double)p[1]);
return;
}
for(int i = 2; i <= n; ++i)
p[i] += p[i - 1];
s[++top] = 0;
s[++top] = 1;
for(int i = 2; i <= n; ++i) {
while(top >= 2) {
int s1 = s[top];
int s2 = s[top - 1];
ll m1 = (p[i] - p[s1]) * (s1 - s2);
ll m2 = (p[s1] - p[s2]) * (i - s1);
//double k1 = 1.0 * (p[i] - p[s1]) / (i - s1);
//double k2 = 1.0 * (p[s1] - p[s2]) / (s1 - s2);
//printf("k1=%.8f k2=%.8f\n", k1, k2);
if(m1 <= m2)
--top;
else
break;
}
s[++top] = i;
}
for(int j = 1; j < top; ++j) {
double avg = 1.0 * (p[s[j + 1]] - p[s[j]]) / (s[j + 1] - s[j]);
sprintf(ans, "%.10f", avg);
int tmp = s[j + 1] - s[j];
while(tmp--)
puts(ans);
}
return;
}
收获:
1、说白了是因为我不会斜率优化才看不出来的。
2、快速输出是真的快,只需要800ms!
3、用前缀和求出下凸包之后,并不一定要用前缀和差分进行构造答案,可以直接统计输出。
4、貌似在用凸包的时候,不需要进行那种+1和-1的操作。