Educational Codeforces Round 4
题目链接:https://codeforces.com/contest/612
A - The Text Splitting
题意:给出n,x,y,求一组解(a,b),使得a和b都是自然数,且ax+by=n。
题解:数据量太小,枚举即可,否则可以使用扩展欧几里得算法,算出一组特解,然后根据通解的构造规律尝试构造两个数都是自然数的一组通解。
B - HDD is Outdated Technology
比A题更无聊。
int pos[200005];
void test_case() {
int n;
scanf("%d", &n);
for(int i = 1, x; i <= n; ++i) {
scanf("%d", &x);
pos[x] = i;
}
ll sum = 0;
for(int i = 2; i <= n; ++i)
sum += abs(pos[i] - pos[i - 1]);
printf("%lld\n", sum);
}
C - Replace To Make Regular Bracket Sequence
题意:有4种括号,其中一次修改可以把一种左括号换成任意一种左括号,或者把一种右括号换成任意一种右括号。问最少的操作次数使得整个串合法。
题解:贪心,每次都修改必须修改的右括号。
char s[1000005];
char stk[1000005];
int top;
void test_case() {
scanf("%s", s + 1);
int n = strlen(s + 1);
if(n % 2 == 1) {
puts("Impossible");
return;
}
int cnt = 0;
for(int i = 1; i <= n; ++i) {
if(s[i] == '(' || s[i] == '<' || s[i] == '[' || s[i] == '{')
++cnt;
else {
--cnt;
if(cnt < 0) {
puts("Impossible");
return;
}
}
}
if(cnt != 0) {
puts("Impossible");
return;
}
top = 0;
int ans = 0;
for(int i = 1; i <= n; ++i) {
if(s[i] == '(' || s[i] == '<' || s[i] == '[' || s[i] == '{')
stk[++top] = s[i];
else {
if(stk[top] == '(' && s[i] == ')' || stk[top] == '<' && s[i] == '>'
|| stk[top] == '[' && s[i] == ']' || stk[top] == '{' && s[i] == '}')
;
else
++ans;
--top;
}
}
printf("%d\n", ans);
}
*D - The Union of k-Segments
题意:x轴上有n条线段,某些点是“满意的”,当且仅当其被至少k条线段覆盖。找一个最小的线段集合,使得所有“满意的”的点都在其中,而没有其他的点。
题解:阅读理解?还好我技高一筹,这里题目要找的“线段集合”并不是输入给的线段。假如坐标比较小可以直接差分。不过这个思路给了个启发:直接分别左端点排序和右端点排序,然后双指针推一推。
不过细节还是蛮多的。最后是用三个指针实现的。双指针分别指向最左的左端点和最左的右端点,取出最小的那个更新cur。根据离散介值定理恰好在更新某点时cnt==k,把这时的cur(一定有cur等于某个左端点)作为答案的左端点。要注意仅1个点的线段也是线段,所以重合的点中要处理完所有的左端点再处理右端点。否则要么左端点已经用完了,要么最左的左端点还比右端点大,这时必须取一个右端点更新cur,更新结束之后再把cnt-1,然后在cnt-1之前判断cnt是否恰好为k,若此时恰好为k则包含cur在内的这条线段就是答案的右端点。
别开小数组,数清楚有几个零。
int l[1000005];
int r[1000005];
int ansl[1000005];
int ansr[1000005];
int atop;
void test_case() {
int n, k;
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; ++i)
scanf("%d%d", &l[i], &r[i]);
sort(l + 1, l + 1 + n);
sort(r + 1, r + 1 + n);
atop = 0;
int p1 = 1, p2 = 1;
int cnt = 0, cur = -INF, L = -INF;
while(1) {
if(cur == r[n]) {
if(cnt >= k) {
++atop;
ansl[atop] = L;
ansr[atop] = r[n];
}
break;
}
if(p1 <= n && l[p1] <= r[p2]) {
cur = l[p1];
++p1;
++cnt;
if(cnt == k)
L = cur;
continue;
}
//p1>n||l[p1]>r[p2]
cur = r[p2];
++p2;
if(cnt == k) {
++atop;
ansl[atop] = L;
ansr[atop] = cur;
}
--cnt;
}
printf("%d\n", atop);
for(int i = 1; i <= atop; ++i)
printf("%d %d\n", ansl[i], ansr[i]);
}
标签:双指针
官方题解有更简单的做法,把左右端点分别设置为两个事件,然后一起排序,位于同一个端点的事件,左端点优先,这样就不需要双指针来比较下一个事件是左端点还是右端点了。
启示:这种题目,区间端点会发生事件是显然的,以后不要把几种事件分别排序然后弄几个指针比来比去,直接放在一起排序。
E - Square Root of Permutation
题意:给一个排列,求他的其中一个平方根。设两个排列p和q,称p=q^2,当对于所有i,都有p[i]=q[q[i]]成立。一个排列可以没有平方根,也可能有多个平方根。多个平方根输出任意一个。
这个排列套排列的形式让人联想到置换群,但是不知道具体怎么搞。打了个表,没发现什么规律。
int p[15], q[15];
bool check(int n) {
for(int i = 1; i <= n; ++i) {
if(q[q[i]] != p[i])
return false;
}
return true;
}
void test_case() {
for(int n = 2; n <= 4; ++n) {
for(int i = 1; i <= n; ++i)
p[i] = i;
do {
printf("p=\n ");
for(int i = 1; i <= n; ++i)
printf("%d%c", p[i], " \n"[i == n]);
for(int i = 1; i <= n; ++i)
q[i] = i;
int suc = 0;
do {
if(check(n)) {
printf("q=\n ");
for(int i = 1; i <= n; ++i)
printf("%d%c", q[i], " \n"[i == n]);
suc = 1;
}
} while(next_permutation(q + 1, q + 1 + n));
if(!suc)
puts("-1");
puts("---\n");
} while(next_permutation(p + 1, p + 1 + n));
}
}
题解:貌似这种类似置换群/排列嵌套的题目都可以转换成一个有向图:考虑一个排列q,对于每个q[i],连接有向边<i,q[i]>。由于这是一个排列,所以终点是互不相同的,得到的不是基环树(或者说,非常特殊的基环树,没有树的部分),而是几个互相分离的环。显而易见某个排列q就是每个点i沿图上走一步。排列q^2就是每个点沿图上走两步。假如某个图走两步的结果就是p,那么这个图是其中一种答案。
既然看到了这里,可以观察到一个显然的事实,无论走多少步,都是环上的点自己在走,
对某个环进行“走两步压缩”,奇数长度的环会得到:
1->2->3->4->5(->1)变成1->3->5->2->4(->1)。
偶数长度的环会得到:
1->2->3->4(->1)变成1->3(->1)和2->4(->2)。
意思就是奇数长度的会发生轮换,偶数长度的会分裂成两个等大的。
那么求出排列p的这个图,也是得到了几个环。那么奇数长度的环就可以直接构造出来,或者和一个等大的奇数长度的环合并成一个大的偶数长度环,比如:
1->2->3->4->5(->1),构造答案为1->4->2->5->3(->1),构造的顺序是,先把1放在最后面(第6个数),然后把1的前一个数5放在第4个数,再把5的前一个数放在第2个数……
偶数长度的就只能够和等大的环合并成更大的偶数长度环了,直接交错即可。
int n;
int nxt[1000005];
bool vis[1000005];
int dfs(int u, int siz) {
vis[u] = 1;
if(vis[nxt[u]] == 0)
return dfs(nxt[u], siz + 1);
return siz;
}
int q[1000005];
void Merge(int r1, int r2, int siz) {
while(siz--) {
q[r1] = r2;
r1 = nxt[r1];
q[r2] = r1;
r2 = nxt[r2];
}
}
int tmp[1000005], top;
void Solve(int r, int siz) {
top = 0;
while(siz--) {
tmp[++top] = r;
r = nxt[r];
}
tmp[top + 1] = tmp[1];
int mid = (top + 1) / 2;
for(int j = 1, i = mid + 1; i <= top + 1; ++j, ++i) {
q[tmp[j]] = tmp[i];
q[tmp[i]] = tmp[j + 1];
}
}
pii SizID[1000005];
int stop;
void test_case() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &nxt[i]);
stop = 0;
for(int i = 1; i <= n; ++i) {
if(vis[i] == 0) {
int siz = dfs(i, 1);
SizID[++stop] = {siz, i};
}
}
sort(SizID + 1, SizID + 1 + stop);
for(int i = 1; i <= stop; ++i) {
if(SizID[i].first % 2 == 1)
Solve(SizID[i].second, SizID[i].first);
else {
if(i + 1 <= stop && SizID[i].first == SizID[i + 1].first) {
Merge(SizID[i].second, SizID[i + 1].second, SizID[i].first);
++i;
} else {
puts("-1");
return;
}
}
}
for(int i = 1; i <= n; ++i)
printf("%d%c", q[i], " \n"[i == n]);
}
好像观察到奇偶性貌似并不影响构造,假如把两个偶数串前后连接,也可以使用奇数的构造方法。