Codeforces Round #542 [Alex Lopashev Thanks-Round] (Div. 1) 题解

A. Toy Train

  • 时间限制:2 seconds
  • 内存限制:256 megabytes

题意

有编号 1 1 1~ n ( n ≤ 5000 ) n(n\le 5000) n(n5000)的车站顺时针呈环排列,有 m ( m ≤ 20000 ) m(m\le 20000) m(m20000)个糖果需要运送到指定位置。每个糖果由 ( a [ i ] , b [ i ] ) (a[i],b[i]) (a[i],b[i])表示,其中 a [ i ] = ̸ b [ i ] a[i]=\not b[i] a[i]≠b[i],指这个糖果最初位置和需要运送到的位置。列车只能顺时针行驶,行驶到相邻站台所用时间为 1 1 1。每到一个站台,最多只能取走一个糖果放在车上,这个糖果可以随便选。而卸下糖果的数量不限制。询问列车从每个位置出发把所有糖果都运送到指定位置的最短时间。

下图中蓝色的数字就表示该糖果要去的位置。
Codeforces Round #542 [Alex Lopashev Thanks-Round] (Div. 1) 题解_第1张图片

分析

假设第 i i i个车站有 s z [ i ] sz[i] sz[i]个糖果,显然对于这个车站必须要至少经过 s z [ i ] sz[i] sz[i]次。那么决定最后在哪个地方停止一定是最后一个糖果。根据贪心的策略,我们只要把最近的糖果最后取就行了。

所以我们枚举起始点 i i i,再枚举每一个车站 j j j,如果 j j j车站有糖果的话,取完车站 j j j的至少需要走的时间就是 d i s t ( i , j ) + ( s z [ j ] − 1 ) ∗ n + min ⁡ a [ k ] = j d i s t ( j , b [ k ] ) dist(i,j)+(sz[j]-1)*n+\min \limits_{a[k]=j} dist(j, b[k]) dist(i,j)+(sz[j]1)n+a[k]=jmindist(j,b[k])。所有的取最大值就是答案了。

d i s t ( i , j ) dist(i,j) dist(i,j)表示从 i i i走到 j j j的距离,具体值等于 ( j − i + n ) % n (j-i+n)\%n (ji+n)%n

代码如下

#include 
using namespace std;
const int MAXN = 5005;
int n, m, sz[MAXN], mn[MAXN];
inline int dist(const int &A, const int &B) { return B >= A ? B-A : n+B-A; }
int main () {
   scanf("%d%d", &n, &m);
   memset(mn, 0x7f, sizeof mn);
   for(int x, y, i = 1; i <= m; ++i) {
       scanf("%d%d", &x, &y); ++sz[x];
       mn[x] = min(mn[x], dist(x, y));
   }
   for(int i = 1; i <= n; ++i) {
       int ans = 0;
       for(int j = 1; j <= n; ++j) if(sz[j])
           ans = max(ans, dist(i, j) + n*(sz[j]-1) + mn[j]);
       printf("%d ", ans);
   }
}

B. Wrong Answer

  • 时间限制:1 seconds
  • 内存限制:256 megabytes
    这题目好啊

题意

有这样一个问题,给出长度为 n n n的整数序列 a a a,求 max ⁡ 1 ≤ l ≤ r ≤ n ∑ l ≤ i ≤ r ( r − l + 1 ) ∗ a i \large \max \limits_{1\le l\le r\le n} \sum_{l\le i\le r}(r-l+1)*a_i 1lrnmaxlir(rl+1)ai
其中 n ≤ 2000 , ∣ a i ∣ ≤ 1 0 6 n\le2000,|a_i|\le10^6 n2000,ai106

A l i c e Alice Alice同学玩够各类博弈游戏发现自己老是输给 B o b Bob Bob,决定卧薪尝胆好好刷题。她遇到了这个问题,然后写了这样一个代码:

function find_answer(n, a)
    # Assumes n is an integer between 1 and 2000, inclusive
    # Assumes a is a list containing n integers: a[0], a[1], ..., a[n-1]
    res = 0
    cur = 0
    k = -1
    for i = 0 to i = n-1
        cur = cur + a[i]
        if cur < 0
            cur = 0
            k = i
        res = max(res, (i-k)*cur)
    return res

显然这个代码是 W r o n g   A n s w e r \color{red}{Wrong\ Answer} Wrong Answer的。举个例子,假设 n = 4 n=4 n=4 a = [ 6 , − 8 , 7 , − 42 ] a=[6,−8,7,−42] a=[6,8,7,42]。那么 f i n d _ a n s w e r ( n , a ) find\_answer(n, a) find_answer(n,a) 得到的答案是 7 7 7, 然正确答案是 3 ⋅ ( 6 − 8 + 7 ) = 15 3⋅(6−8+7)=15 3(68+7)=15

给出 k ( 1 ≤ k ≤ 1 0 9 ) k(1\le k\le 10^9) k(1k109),你要构造一组数据,使得 A l i c e Alice Alice的答案和正确答案相差恰好为 k k k。你构造的序列必须也满足上面所说的 n ≤ 2000 , ∣ a i ∣ ≤ 1 0 6 n\le2000,|a_i|\le10^6 n2000,ai106。如果不存在请输出 − 1 -1 1

分析

可以看出 A l i c e Alice Alice的程序只统计了非负数的区间。那么我们只需要搞一个 − 1 -1 1在最前面,然后让后面序列的和 S u m Sum Sum尽量的大。这是 A l i c e Alice Alice的答案一定是的和 S u m Sum Sum乘以后面的长度 L L L,但是多取这个 − 1 -1 1会让答案变成 ( S u m − 1 ) ∗ ( L + 1 ) = S u m ∗ L + S u m − L − 1 (Sum-1)*(L+1)=Sum*L+Sum-L-1 (Sum1)(L+1)=SumL+SumL1。我们只要保证 S u m − L − 1 = k Sum-L-1=k SumL1=k就行了,即 S u m = L + 1 + k Sum=L+1+k Sum=L+1+k

那么我们构造出来的序列除去前面的-1就是长度为 L L L,和为 L + 1 + k L+1+k L+1+k的序列。因为我们实际上是可以补0的,那么要让后面的 L L L个数和为 L + 1 + k L+1+k L+1+k。只要前面尽量放 1 0 6 10^6 106,就行了。不过由于这个 1 0 6 10^6 106的限制,要求 L + 1 + k L ≤ 1 0 6 \large \frac{L+1+k}L\le 10^6 LL+1+k106,因为 k ≤ 1 0 9 k\le10^9 k109 L ≤ 2000 L\le 2000 L2000,所以 L L L随便取个 1200 1200 1200就行了。

代码如下

#include 
using namespace std;
int main () {
    int k;
    scanf("%d", &k);
    //sum*1200 = (sum-1)*1201 - k
    //sum = k + 1201
    int sum = k + 1201;
    printf("1201\n-1");
    for(int i = 1; i <= 1200; ++i) {
        int ans = min(1000000, sum);
        sum -= ans;
        printf(" %d", ans);
    }
}

C. Morse Code

  • 时间限制:2 seconds
  • 内存限制:256 megabytes

题意

把二十六个字母的摩尔斯电码用二进制表示就是一些 01 01 01串。摩尔斯电码长度从 1 1 1~ 4 4 4不等,但是这样算出来是 2 1 + 2 2 + 2 3 + 2 4 = 30 = 26 + 4 2^1+2^2+2^3+2^4=30=26+4 21+22+23+24=30=26+4,那么就有 4 4 4个串没有所代表的字母。它们分别是" 0011 0011 0011"," 0101 0101 0101"," 1110 1110 1110",和" 1111 1111 1111"。

最初你有一个空串,有 n ( n ≤ 3000 ) n(n\le 3000) n(n3000)次添加,每次添加 1 / 0 1/0 1/0,每添加一次都要输出当前得到的串的所有子串能够翻译出的不同的字母串的数量,答案膜 1 0 9 + 7 10^9+7 109+7输出。比如"111"能够翻译出的就是这些:
“T” (translates into “1”)
“M” (translates into “11”)
“O” (translates into “111”)
“TT” (translates into “11”)
“TM” (translates into “111”)
“MT” (translates into “111”)
“TTT” (translates into “111”)

分析

假设你有一个固定的串,求它能够翻译出的不同字母串的dp还是很好想的。时间复杂度是 O ( l e n ∗ 4 ) O(len*4) O(len4)。那么一个暴力的想法就是枚举每个不同子串做dp,这样dp的总时间复杂度是 O ( n 3 ) O(n^3) O(n3)

然而我们发现当左端点 l l l固定的时候,右端点 r r r递增时其实可以一起dp的。那么对于左端点固定的子串,dp就是 O ( n ) O(n) O(n)的。

然而这个是每一次在最后添加一个字符。不能固定左端点。

怎么办呢?

那就固定右端点倒着dp呗。

然后我判重用 h a s h + m a p hash+map hash+map,多了一个 l o g log log再加上dp有个 4 4 4的常数就 T L E TLE TLE了。

怎么办呢,这时候又用到上面那个共用的思想(…),我们用trie树来判 01 01 01串的重,如果到最后一个字符的个节点已经有值了那么已经算过,就不加到答案里面;否则就统计答案。右端点固定的话左端点递减的串在trie树上是一条路径,这样的话插入这些右端点固定的子串时间复杂度就是 O ( n ) O(n) O(n)的了。

右端点有 n n n个,那么总时间复杂度就是 O ( n 2 ) O(n^2) O(n2)的了。

代码如下

#include 
using namespace std;
const int MAXN = 3010;
const int mod = 1e9 + 7;
int n, x[MAXN], dp[MAXN], ans, trie[MAXN*MAXN][2];
inline bool chk(int i) {
    if(!x[i-3] && !x[i-2] && x[i-1] && x[i]) return 0;
    if(!x[i-3] && x[i-2] && !x[i-1] && x[i]) return 0;
    if(x[i-3] && x[i-2] && x[i-1] && !x[i]) return 0;
    if(x[i-3] && x[i-2] && x[i-1] && x[i]) return 0;
    return 1;
}
inline void add(int &x, int y) { x += y; if(x >= mod) x -= mod; }
int main () {
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &x[i]);
    int rt = 1, cnt = 1;
    for(int i = 1; i <= n; ++i) {
        dp[i+1] = 1;
        int r = rt;
        for(int j = i; j; --j) {
        	dp[j] = 0;
            add(dp[j], dp[j+1]);
            add(dp[j], dp[j+2]);
            add(dp[j], dp[j+3]);
            if(chk(j+3)) add(dp[j], dp[j+4]);
            if(!trie[r][x[j]]) trie[r][x[j]] = ++cnt, add(ans, dp[j]);
            r = trie[r][x[j]];
        }
        printf("%d\n", ans);
    }
}

D. Isolation

  • 时间限制:3 seconds
  • 内存限制:256 megabytes

题意

给出长度为 n ( 1 ≤ n ≤ 1 0 5 ) n(1\le n\le 10^5) n(1n105)的数组 a ( a i ≤ n ) a(a_i\le n) a(ain)。给出 1 ≤ k ≤ n 1\le k\le n 1kn。求把这个序列分成若干块的方案,使得对于每个区间,只出现一次的数字不超过 k k k个。

分析

首先暴力 O ( n 2 ) O(n^2) O(n2)dp很好想的,转移方程式为:
d p [ i ] + = d p [ j − 1 ]   且 ( i , j ) dp[i]+=dp[j-1]\ 且(i,j) dp[i]+=dp[j1] (i,j)中只出现一次的数不超过 k k k

我们看看怎么记录只出现一次的数的个数,就是从后往前扫,遇到数的第一次+1,第二次-1,然后做一个后缀和就行了。那么对于后缀和 ≤ k \le k k的位置 j j j j − 1 j-1 j1就能够当作决策点。为了方便我们把 d p [ ] dp[] dp[]的下标同时加一就行了。

这样的话每往后移动就修改两个区间(画画就知道了),区间修改可以用线段树,但是穿插着查询的话无法处理,于是分块做。每个块存一下后缀和为每个值的 d p dp dp值的和,也要存一下后缀和 ≤ k \le k k d p dp dp值的和,也就是这个块内的答案。也要维护懒标记。

时间复杂度 O ( n n ) O(n\sqrt n) O(nn )

代码如下

#include
using namespace std;
typedef long long LL;
const int mod = 998244353;
const int MAXN = 100005;
const int Blocks = 405;
int n, qk, a[MAXN], pre[MAXN], lst[MAXN], dp[MAXN], f[MAXN];
int sum[Blocks][MAXN], ans[Blocks], lz[Blocks], l[Blocks], r[Blocks], bl[MAXN];
inline void add(int &x, int y) { x += y; x = x >= mod ? x - mod : x; }
inline void sub(int &x, int y) { x -= y; x = x < 0 ? x + mod : x; }
inline void Rebuild(int x) {
    for(int k = l[x]; k <= r[x]; ++k) sum[x][f[k]] = 0, f[k] += lz[x];
    lz[x] = ans[x] = 0;
    for(int k = l[x]; k <= r[x]; ++k) {
        add(sum[x][f[k]], dp[k]);
        if(f[k] <= qk) add(ans[x], dp[k]);
    }
}
inline void Update(int k, int val) {
    int x = bl[k];
    sub(sum[x][f[k]], dp[k]);
    if(f[k] <= qk) sub(ans[x], dp[k]);
    f[k] += val;
    add(sum[x][f[k]], dp[k]);
    if(f[k] <= qk) add(ans[x], dp[k]);
}
inline void Modify(int x, int y, int val) {
    int L = bl[x], R = bl[y];
    if(L == R) {
        Rebuild(L);
        for(int k = x; k <= y; ++k)
            Update(k, val);
    }
    else {
        Rebuild(L);
        for(int k = x; k <= r[L]; ++k) Update(k, val);
        Rebuild(R);
        for(int k = l[R]; k <= y; ++k) Update(k, val);
        for(int i = L+1; i < R; ++i)
            if(val == 1) {
                int tmp = qk - (lz[i]++);
                sub(ans[i], sum[i][tmp]);
            }
            else {
                int tmp = qk - (--lz[i]);
                add(ans[i], sum[i][tmp]);
            }
    }
}
inline int Query(int x) {
    int re = 0;
    Rebuild(bl[x]);
    for(int k = l[bl[x]]; k <= x; ++k)
        if(f[k] <= qk) add(re, dp[k]);
    for(int i = 1; i < bl[x]; ++i) add(re, ans[i]);
    return re;
}
int main() {
	scanf("%d%d", &n, &qk);
	for(int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        pre[i] = lst[a[i]];
        lst[a[i]] = i;
	}
	dp[1] = 1;
	int m = sqrt(n);
	for(int i = 1; i <= n; ++i) {
        bl[i] = (i-1)/m + 1;
        r[bl[i]] = i;
        if(!l[bl[i]])
            l[bl[i]] = i;
	}
	for(int i = 1; i <= n; ++i) {
        Modify(pre[i]+1, i, 1);
        if(pre[i])
            Modify(pre[pre[i]]+1, pre[i], -1);
        dp[i+1] = Query(i);
	}
	printf("%d\n", dp[n+1]);
}

写加法减法的时候用问号语句600ms,用if语句1400ms。。


E. Legendary Tree

  • 时间限制:3 seconds
  • 内存限制:256 megabytes

T h i s   i s   a n   i n t e r a c t i v e   p r o b l e m . \it{This\ is\ an\ interactive\ problem.} This is an interactive problem.

题意

给出树的节点数 n ( ≤ 500 ) n(\le 500) n(500),你的任务是确定树的结构。

你可以询问 11111 11111 11111次,每次查询 < { S } , { T } , v > <\{S\},\{T\},v> <{S},{T},v>,格式如下
l i n e   1 : ∣ S ∣ line\ 1:|S| line 1:S
l i n e   4 : S 1 , S 2 , S 3 . . . S ∣ S ∣ line\ 4:S_1,S_2,S_3...S_{|S|} line 4:S1,S2,S3...SS
l i n e   3 : ∣ T ∣ line\ 3:|T| line 3:T
l i n e   4 : T 1 , T 2 , T 3 . . . T ∣ T ∣ line\ 4:T_1,T_2,T_3...T_{|T|} line 4:T1,T2,T3...TT
l i n e   5 : v line\ 5:v line 5:v
表示询问从 ∣ S ∣ |S| S集的某个点经过 v v v走到 ∣ T ∣ |T| T集的某个点的简单路径条数。其中 S ⋂ T = ∅ S\bigcap T=\varnothing ST=

分析

先把1设为根

那么我们只要查询 n n n < { 1 } , { 2... n } , i > <\{1\},\{2...n\},i> <{1},{2...n},i> 就能得到 i i i的子树大小 s z [ i ] sz[i] sz[i]

我们的目标是找到所有个节点的父亲

显然父亲的 s z sz sz比儿子大

我们把2~n的节点按 s z sz sz排序

用一个vector存储到目前还没有找到父亲的点,这些点实际上可以看做叶节点,因为那些找到了父亲的点已经被我们从vector内删去。

s z sz sz从小到大考虑,当前考虑到第 i i i个点,如果它有儿子,那么一定存在于vector中。如果 i i i j j j的父亲,那么一定存在从 1 − > i − > j 1->i->j 1>i>j的路径,反之一定不存在(不可能有隔代的路径,因为我们是按 s z sz sz从小到大考虑,隔代的情况不会出现 因为中间那一代 s z sz sz小会被先考虑)。所以我们可以用一次查询 < { 1 } , { S } , i > <\{1\},\{S\},i> <{1},{S},i>来判断叶节点集 S S S内是否有 i i i的儿子。那么我们二分找到最左边的一个儿子,将它删去,并把它的父亲设为 i i i。这样一直做下去,就行了。一个点只会被删一次,查询 l o g   n log\ n log n次。

总查询次数是 O ( n + n   l o g n ) O(n+n\ logn) O(n+n logn)
时间复杂度是 O ( n 2 l o g n ) O(n^2logn) O(n2logn)

#include
using namespace std;
vector<int>S, T, vec;
#define pb push_back
const int MAXN = 505;
int n, sz[MAXN], c[MAXN], fa[MAXN];
inline int query(int x) {
    printf("%d\n", S.size());
    for(auto v:S) printf("%d ", v);
    printf("\n%d\n", T.size());
    for(auto v:T) printf("%d ", v);
    printf("\n%d\n", x);
    fflush(stdout);
    int re; scanf("%d", &re);
	return re;
}
inline bool chk(int x, int pos) {
	T.clear();
	for(int i = 0; i <= pos; ++i)
		T.pb(vec[i]);
	return query(x);
}
inline void getchild(int x) {
	while(!vec.empty()) {
		int l = 0, r = vec.size()-1, pos = -1;
		while(l <= r) {
			int mid = (l + r) >> 1;
			if(chk(x, mid)) pos = mid, r = mid-1;
			else l = mid+1;
		}
		if(~pos) {
			fa[vec[pos]] = x;
			swap(vec[pos], vec.back());
			vec.pop_back();
		}
		else return;
	}
}
inline bool cmp(int A, int B) { return sz[A] < sz[B]; }
int main() {
	scanf("%d", &n);
	S.pb(1);
	for(int i = 2; i <= n; ++i) T.pb(i), c[i-1] = i;
	for(int i = 2; i <= n; ++i) sz[i] = query(i);
	sort(c + 1, c + n, cmp);
	for(int i = 1; i < n; ++i) {
		int x = c[i];
		getchild(x);
		vec.pb(x);
	}
	puts("ANSWER");
	for(int i = 2; i <= n; ++i)
		printf("%d %d\n", fa[i]?fa[i]:1, i);
}//最后自动fflush

这道题200组数据又是交互题很慢直接评测了5min。。。

你可能感兴趣的:(CF题解)