Codeforces Round #600 (Div. 2)

A - Single Push

题意:给数组a和数组b,可以选择一段连续的区间[l,r]使得ai全部加k(k>0)至多一次。求能不能从a变成b。

题解:一开始作差排序去重,然后判断差是不是只有1个(且>=0)或只有两个(且c1=0,c2>0),但这样是错的,比如下面的样例。

1
5
1 1 1 1 1
2 1 2 2 2

因为虽然差都是1但不是连续的区间。

做法是作差,然后判断是否有至多一个正的方波。可以用两个变量,一个记录是否进入了方波,且方波的高是多少。另一个记录是否离开了方波。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
 
const int mod = 1e9 + 7;
 
ll pow_mod(ll x, int n) {
    ll res = 1;
    while(n) {
        if(n & 1)
            res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}
 
int n, a[100005], b[100005];
 
void test_case() {
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &b[i]);
        b[i] -= a[i];
    }
    int in = 0, out = 0;
    for(int i = 1; i <= n; ++i) {
        if(b[i] < 0) {
            puts("NO");
            return;
        } else if(b[i] > 0) {
            if(in == 0)
                in = b[i];
            else if(out || in != b[i]) {
                puts("NO");
                return;
            }
        } else if(in == 1)
            out = 1;
    }
    puts("YES");
    return;
}
 
int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t = 1;
    scanf("%d", &t);
    for(int ti = 1; ti <= t; ++ti) {
        //printf("Case #%d: ", ti);
        test_case();
    }
}

但第二天早上发现方波其实是什么?数列后面加上一个0,那么一个方波的差分就一定至多各有一个+a和-a。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mod = 1e9 + 7;

ll pow_mod(ll x, int n) {
    ll res = 1;
    while(n) {
        if(n & 1)
            res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}

int n, a[100005], b[100005];

void test_case() {
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &b[i]);
        b[i] -= a[i];
    }
    b[n + 1] = 0;
    for(int i = n + 1; i >= 1; --i)
        b[i] -= b[i - 1];

    int tmp = 0;
    for(int i = 1; i <= n + 1; ++i) {
        if(b[i] > 0) {
            if(tmp == 0)
                tmp = b[i];
            else {
                puts("NO");
                return;
            }
        } else if(b[i] < 0) {
            if(tmp == -b[i])
                tmp = -1;
            else {
                puts("NO");
                return;
            }
        }
    }
    if(tmp == 0 || tmp == -1)
        puts("YES");
    else
        puts("NO");
    return;
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t = 1;
    scanf("%d", &t);
    for(int ti = 1; ti <= t; ++ti) {
        //printf("Case #%d: ", ti);
        test_case();
    }
}

假如把n+1的项去掉,也可以“检测不到bi<0”则认为成功。

还有一种写法是,作差之后把两边的0缩掉,然后判断中间的区间都全等。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mod = 1e9 + 7;

ll pow_mod(ll x, int n) {
    ll res = 1;
    while(n) {
        if(n & 1)
            res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}

int n, a[100005], b[100005];

void test_case() {
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &b[i]);
        b[i] -= a[i];
    }
    int L = 1, R = n;
    while(L <= R && b[L] == 0)
        ++L;
    while(L <= R && b[R] == 0)
        --R;
    bool suc = 1;
    if(L <= R) {
        if(b[L] < 0)
            suc = 0;
        else {
            for(int i = L; i <= R; ++i) {
                if(b[i] != b[L]) {
                    suc = 0;
                    break;
                }
            }
        }
    }
    if(suc)
        puts("YES");
    else
        puts("NO");
    return;
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t = 1;
    scanf("%d", &t);
    for(int ti = 1; ti <= t; ++ti) {
        //printf("Case #%d: ", ti);
        test_case();
    }
}

B - Silly Mistake

题意:有个打卡机,序号a的人打卡上班+a,打卡下班-a。但是分隔天数的记录没了。规定每天每人只能上班至多一次,且上班后必须下班。求一种划分天数的方法,无解输出-1。贪心构造一种划分天数最多的方法,每次所有人下班之后就开一天新的,这样不容易导致同一个人上班多次(当然假如遇到这个上班过的人再开一天新的就是划分天数最少的方法)。记得最后一天要判断所有人都下班了!

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mod = 1e9 + 7;

ll pow_mod(ll x, int n) {
    ll res = 1;
    while(n) {
        if(n & 1)
            res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}


int n, a[100005], cnt[1000005], vis[1000005];
int ans[50005], atop;

void test_case() {
    //memset(cnt, 0, sizeof(cnt));
    //memset(vis, 0, sizeof(vis));

    scanf("%d", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);

    atop = 0;
    int pre = 0, cntm = 0;
    for(int i = 1; i <= n; ++i) {
        if(a[i] > 0) {
            ++cnt[a[i]];
            ++vis[a[i]];
            if(cnt[a[i]] == 1)
                ++cntm;
            else {
                puts("-1");
                return;
            }
        } else {
            --cnt[-a[i]];
            if(cnt[-a[i]] == 0) {
                --cntm;
                if(cntm == 0) {
                    for(int k = pre + 1; k <= i; ++k) {
                        if(a[k] < 0)
                            continue;
                        if(vis[a[k]] > 1) {
                            puts("-1");
                            return;
                        }
                        vis[a[k]] = 0;
                    }
                    ans[++atop] = i;
                    pre = i;
                }
            } else {
                puts("-1");
                return;
            }
        }
    }
    if(cntm != 0) {
        puts("-1");
        return;
    }

    for(int i = atop; i >= 1; --i)
        ans[i] -= ans[i - 1];
    printf("%d\n", atop);
    for(int i = 1; i <= atop; ++i)
        printf("%d%c", ans[i], " \n"[i == atop]);
    return;
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t = 1;
    //scanf("%d", &t);
    for(int ti = 1; ti <= t; ++ti) {
        //printf("Case #%d: ", ti);
        test_case();
    }
}

当然去重还是用nlogn的方法最方便,比如来个set,insert之前看一下,这样写起来(或许)快,没有上面的这么复杂,比赛时用的是vector。

真的要小心边界,为什么会漏开头和结尾呢?写了这么多次CF了还是翻车。

C - Sweets Eating

题意:给n颗糖,每颗糖含糖量ai,每天至多吃m颗糖,在第d天吃第i颗糖会收获含糖量d/*ai,对于总共要吃1,2,3,...,n颗糖,输出每个值最小的含糖量。

题解:首先假如只问1次吃k颗糖,那么肯定要选择含糖最小的k颗出来,然后貌似很显然的贪心,就是小的配大的,大的配小的,这样总的最大。

比如这样证明:

两个数组 \(a\)\(b\) ,对它们重新排列以最小化 \(\sum\limits_{i=1}^{n}a_ib_i\)

很显然排列可以分解成若干个交换操作。考虑什么时候要交换。

\(a_ib_i+a_jb_j>=a_ib_j+a_jb_i\) 时,交换两个数的配对不会更差。

移项得 \((a_i-a_j)(b_i-b_j)>=0\)

提醒我们要大的配小的,这样经过若干次交换之后会达到最小。

但是暴力求解会T掉,考虑贡献的转移。以样例为例,糖果为:

2 3 4 4 6 6 7 8 19

考虑贡献的序列:

1
1 1
2 1 1
2 2 1 1
3 2 2 1 1
3 3 2 2 1 1
...

可以看到相隔m行的,作差之后恰好剩下一个前缀和。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mod = 1e9 + 7;

ll pow_mod(ll x, int n) {
    ll res = 1;
    while(n) {
        if(n & 1)
            res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}

int a[200005];
ll prefix[200005], sum[200005];

void test_case() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    sort(a + 1, a + 1 + n);
    for(int i = 1; i <= n; ++i)
        prefix[i] = prefix[i - 1] + a[i];
    for(int i = 1; i <= m; ++i)
        sum[i] = prefix[i];
    for(int i = m + 1; i <= n; ++i)
        sum[i] = sum[i - m] + prefix[i];
    for(int i = 1; i <= n; ++i)
        printf("%lld%c", sum[i], " \n"[i == n]);
    return;
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t = 1;
    //scanf("%d", &t);
    for(int ti = 1; ti <= t; ++ti) {
        //printf("Case #%d: ", ti);
        test_case();
    }
}

事实上只需要快排的一点点额外空间。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mod = 1e9 + 7;

ll pow_mod(ll x, int n) {
    ll res = 1;
    while(n) {
        if(n & 1)
            res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}

ll a[200005];

void test_case() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i)
        scanf("%lld", &a[i]);
    sort(a + 1, a + 1 + n);
    for(int i = 1; i <= n; ++i)
        a[i] += a[i - 1];
    for(int i = m + 1; i <= n; ++i)
        a[i] += a[i - m];
    for(int i = 1; i <= n; ++i)
        printf("%lld%c", a[i], " \n"[i == n]);
    return;
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t = 1;
    //scanf("%d", &t);
    for(int ti = 1; ti <= t; ++ti) {
        //printf("Case #%d: ", ti);
        test_case();
    }
}

D - Harmonious Graph

题意:一个n点m边无向图,称一个无向图是和谐的,当其对所有的点l,r,m(l<m<r),满足若l与r互达,则l与m互达。

题解:若l与r互达,则l与m互达,相当于l,r,m都同一个连通块。意思是补上一些边把这堆数分成几个连续的段。可以这样做:每个点维护它所属的连通块的最大点,然后循环到最大点位置,不断尝试连边,注意中途可能会连上别的东西导致最大点改变,比如1-4,2-5,连了1-2之后1的最大点从4变成5。维护连通块的性质上并查集。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mod = 1e9 + 7;

ll pow_mod(ll x, int n) {
    ll res = 1;
    while(n) {
        if(n & 1)
            res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}

struct DisjointSetUnion {
    static const int MAXN = 200000;
    int n, fa[MAXN + 5], rnk[MAXN + 5];
    int maxid[MAXN + 5];

    void Init(int _n) {
        n = _n;
        for(int i = 1; i <= n; i++) {
            fa[i] = i;
            rnk[i] = 1;
            maxid[i] = i;
        }
    }

    int Find(int u) {
        int r = fa[u];
        while(fa[r] != r)
            r = fa[r];
        int t;
        while(fa[u] != r) {
            t = fa[u];
            fa[u] = r;
            u = t;
        }
        return r;
    }

    bool Merge(int u, int v) {
        u = Find(u), v = Find(v);
        if(u == v)
            return false;
        else {
            if(rnk[u] < rnk[v])
                swap(u, v);
            fa[v] = u;
            rnk[u] += rnk[v];
            maxid[u] = max(maxid[u], maxid[v]);
            return true;
        }
    }

    void solve() {
        int ans = 0;
        for(int i = 1; i <= n; ++i) {
            while(i < maxid[Find(i)]) {
                ans += Merge(i, i + 1);
                ++i;
            }
        }
        printf("%d\n", ans);
    }
} dsu;

void test_case() {
    int n, m;
    scanf("%d%d", &n, &m);
    dsu.Init(n);
    for(int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        dsu.Merge(u, v);
    }
    dsu.solve();
    return;
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t = 1;
    //scanf("%d", &t);
    for(int ti = 1; ti <= t; ++ti) {
        //printf("Case #%d: ", ti);
        test_case();
    }
}

E - Antenna Coverage

题意:一个直线区域1,m,上面有n(n<=80)个天线(路由器?),天线有位置xi和半径ri,覆盖[xi-ri,xi+ri]区域,花费1单位可以增加某个天线1半径,求覆盖整个区域的最少花费,注意覆盖范围可以越界。

题解:首先加入一个天线在0号位置,半径也是0,很显然这样并不会改变最终答案因为选择其他天线扩张永远更好(比如在m位置的0半径都只需要m-1单位)。这样加入之后,就保证了每个点左侧都至少有一个天线,使得转移的边界变少。设dp[i]为把区域[1,i]完全覆盖且只使用左侧的天线所需要的最少花费。

那么我们观察得到一个转移:

对于某个i点左侧的天线j,以下转移:
\(cost=max(0,i-(x[j]+r[j]))\)
\(lb=max(0, min(x[j]-(i-x[j]),x[j]-r[j])\)
\(dp[i]=cost+min{dp[lb-1]~dp[i-1]}\)

注:lb是leftbound的意思,是这个j天线覆盖范围的左边界。上面这个确实是从lb-1转移,因为[1,lb-1]被dp[lb-1]覆盖,而[lb,i]被j覆盖,连起来确实是到i,假如少了dp[lb-1]的值会出现一点不妥,在样例输入3的时候会发现有点不对劲。

对于这个天线j,貌似在前面的dp中可能会用过,貌似会影响无后效性,但是其实让一个天线反复增加一段相同的一定是不好的,在rmq的时候会找到。

补题1A???需要注意因为dp[0]的存在线段树需要偏移一个位置。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
 
const int mod = 1e9 + 7;
 
ll pow_mod(ll x, int n) {
    ll res = 1;
    while(n) {
        if(n & 1)
            res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}
 
const int INF = 0x3f3f3f3f;
const int MAXN = 100000;
int dp[MAXN + 5];
 
struct SegmentTree {
#define ls (o<<1)
#define rs (o<<1|1)
    static const int MAXN = 100000;
    int st[(MAXN << 2) + 5];
 
    void PushUp(int o) {
        st[o] = min(st[ls], st[rs]);
    }
 
    void Build(int o, int l, int r) {
        if(l == r)
            st[o] = dp[l];
        else {
            int m = l + r >> 1;
            Build(ls, l, m);
            Build(rs, m + 1, r);
            PushUp(o);
        }
    }
 
    void Update(int o, int l, int r, int p, int v) {
        if(l == r) {
            st[o] = v;
            return;
        } else {
            int m = l + r >> 1;
            if(p <= m)
                Update(ls, l, m, p, v);
            if(p >= m + 1)
                Update(rs, m + 1, r, p, v);
            PushUp(o);
        }
    }
 
    int Query(int o, int l, int r, int ql, int qr) {
        if(ql <= l && r <= qr) {
            return st[o];
        } else {
            int m = l + r >> 1;
            int res = INF;
            if(ql <= m)
                res = min(res, Query(ls, l, m, ql, qr));
            if(qr >= m + 1)
                res = min(res, Query(rs, m + 1, r, ql, qr));
            return res;
        }
    }
#undef ls
#undef rs
} st;
 
 
struct Artenna {
    int x, s;
    bool operator<(const Artenna& a)const {
        return x < a.x;
    }
} a[85];
 
void test_case() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i)
        scanf("%d%d", &a[i].x, &a[i].s);
    ++n;
    a[n].x = 0, a[n].s = 0;
    sort(a + 1, a + 1 + n);
 
    memset(dp, INF, sizeof(dp[0]) * (m + 2));
    st.Build(1, 1, m + 1);
    dp[0] = 0;
    st.Update(1, 1, m + 1, 0 + 1, dp[0]);
    for(int i = 1; i <= m; ++i) {
        for(int j = 1; j <= n; ++j) {
            if(a[j].x <= i) {
                int cost = max(0, i - (a[j].x + a[j].s));
                int lb = max(0, min(a[j].x - (i - a[j].x), a[j].x - a[j].s));
                int tmp = cost + st.Query(1, 1, m + 1, lb - 1 + 1, i - 1 + 1);
                dp[i] = min(dp[i], tmp);
            }
        }
        st.Update(1, 1, m + 1, i + 1, dp[i]);
    }
    printf("%d\n", dp[m]);
    return;
}
 
int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t = 1;
    //scanf("%d", &t);
    for(int ti = 1; ti <= t; ++ti) {
        //printf("Case #%d: ", ti);
        test_case();
    }
}

算法的瓶颈在于线段树rmq。

看别人的代码貌似都是另一种思路的,怪不得有个sorting的标签。加入0号天线后排序,设dp[i][j]为使用前i个天线,覆盖范围为[1,j]的最小代价,答案为dp[n+1][m](因为多了一个天线),边界为dp[1][i]=i。

转移分为三种情况,每次枚举j从0到m,然后判断j与第i个天线管辖的l,r的关系。

若j<l,则天线需要向左右花费c=x-(j+1)-s延长到j+1,然后连上dp[i-1][j],就可以最远更新到dp[i][x+(x-(j+1))]。注意这个更新实际上是范围更新!
若l<=j<=r,则dp[i-1][j]直接贡献给dp[i][r]就可以,又因为dp是非降序的,所以直接把dp[i-1][l]给dp[i][r]。
若j>r,则天线需要向左右花费c=j-x-s延长到j,然后从dp[i-1][x-(j-x)-1]连接。注意这个更新实际上是范围更新!

然后更新完之后把倒着扫描把范围更新落实下去。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mod = 1e9 + 7;

ll pow_mod(ll x, int n) {
    ll res = 1;
    while(n) {
        if(n & 1)
            res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}

struct Artenna {
    int x, s;
    bool operator<(const Artenna& a)const {
        return x < a.x;
    }
} a[85];

const int INF = 0x3f3f3f3f;
int dp[2][100005];

void test_case() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i)
        scanf("%d%d", &a[i].x, &a[i].s);
    ++n;
    a[n].x = 0, a[n].s = 0;
    sort(a + 1, a + 1 + n);
    memset(dp[0], INF, sizeof(dp[0][0]) * (m + 1));
    memset(dp[1], INF, sizeof(dp[1][0]) * (m + 1));
    for(int j = 0; j <= m; ++j)
        dp[1][j] = j;
    for(int i = 2; i <= n; ++i) {
        for(int j = 0; j <= m; ++j)
            dp[i & 1][j] = min(dp[i & 1][j], dp[(i - 1) & 1][j]);
        int l = max(0, a[i].x - a[i].s), r = min(m, a[i].x + a[i].s);
        for(int j = 0; j < l; ++j) {
            int p = min(m, a[i].x + (a[i].x - (j + 1)));
            int c = max(0, a[i].x - (j + 1) - a[i].s);
            dp[i & 1][p] = min(dp[i & 1][p], dp[(i - 1) & 1][j] + c);
        }

        dp[i & 1][r] = min(dp[i & 1][r], dp[(i - 1) & 1][l]);

        for(int j = r + 1; j <= m; ++j) {
            int p = max(0, a[i].x - (j - a[i].x) - 1);
            int c = max(0, j - a[i].x - a[i].s);
            dp[i & 1][j] = min(dp[i & 1][j], dp[(i - 1) & 1][p] + c);
        }

        for(int j = m - 1; j >= 0; --j)
            dp[i & 1][j] = min(dp[i & 1][j], dp[i & 1][j + 1]);
    }
    printf("%d\n", dp[n & 1][m]);
    return;
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t = 1;
    //scanf("%d", &t);
    for(int ti = 1; ti <= t; ++ti) {
        //printf("Case #%d: ", ti);
        test_case();
    }
}
posted @ 2019-11-17 11:57  KisekiPurin2019  阅读(193)  评论(0编辑  收藏  举报