省选测试14

省选测试14

T1

你有一个字符串 \(S_0\)​,想要用它造出共 n 个新字符串 \(S_{0},\cdots,S_{n-1}​\)

你会按顺序进行 n-1 次操作,造出新字符串。第 i 次操作有两种方法:

S x l r 截取 \(S_x\)​ 的 [l,r) 子串,即第 l 到第 r-1 个字符作为 \(S_i\)

A x y 将 \(S_x\)\(S_y\) 拼接作为 \(S_i\)


最后,你会关心 \(S_{n-1}\)​ 长什么样,因此请输出 \(S_{n-1}\)​ 所有字符的 ASCII 码之和,结果对 \(10^9+7\)取模。

本题中字符串下标均从 0 开始编号,且保证所有字符串串长不超过 \(2^{63}-1\),并且所有操作均合法(即\(0\leq x,y<i,0\leq l < r\leq Len(S_x)\)

对于全部数据,\(n\leq 2000, 1\leq Len(S_0)\leq 2000\),同时满足题目描述中给的限制。

搜索 + 记忆化.

md传参类型传错了少了40pts...

直接分情况搜索就好了, 记录一个第一个串的前缀和.

不过我写的记搜比较慢, 我只记录了一个第\(i\)个字符串的整个的价值是多少, 但是题解记录的是第\(i\)个字符串前\(l\)个字符的价值是多少, 可能用的空间大了点(但这题不卡空间).

#include <bits/stdc++.h>

#define ull unsigned long long

using namespace std;

inline ull read() {
    ull s = 0, f = 1; char ch;
    while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
    for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
    return s * f;
}

const int N = 2005, mod = 1e9 + 7;
int n;
int sum[N], ans[N];
ull len[N];
char ch[N];
struct ques { int opt, x; ull l, r; } q[N];

int calc_qj(int x, ull l, ull r) {
    if(x == 0) return (sum[r] - sum[l - 1] + mod) % mod;
    if(r - l + 1 == len[x]) return ans[x];
    if(q[x].opt == 2) {
        if(r <= len[q[x].l]) return calc_qj(q[x].l, l, r);
        else if(l > len[q[x].l]) return calc_qj(q[x].r, l - len[q[x].l], r - len[q[x].l]);
        else return (calc_qj(q[x].l, l, len[q[x].l]) + calc_qj(q[x].r, 1, r - len[q[x].l])) % mod;
    }
    if(q[x].opt == 1) {
        return calc_qj(q[x].x, l + q[x].l - 1, r + q[x].l - 1);
    }
}

int calc(int x) {
    if(ans[x]) return ans[x];
    // cout << x << " " << q[x].l << " " << q[x].r << "\n";
    if(q[x].opt == 2) {
        ans[x] = (calc(q[x].l) + calc(q[x].r)) % mod;
        len[x] = len[q[x].l] + len[q[x].r];
    }
    else {
        ans[x] = calc_qj(q[x].x, q[x].l, q[x].r);
        len[x] = q[x].r - q[x].l + 1;
        // cout << x << ":" << q[x].r << " " << q[x].l << "\n";
    }
    return ans[x];
}

int main() {

    // freopen("string.in","r",stdin); freopen("string.out","w",stdout);

    scanf("%d", &n);
    scanf("%s", ch + 1);
    int len_ch = strlen(ch + 1);
    for(int i = 1;i <= len_ch; i++) sum[i] = (sum[i - 1] + ch[i]) % mod;
    ans[0] = sum[len_ch]; len[0] = len_ch;
    // for(int i = 1;i <= len_ch; i++) cout << i << ":" << sum[i] << "\n";
    for(int i = 1;i < n; i++) {
        char ch_; cin >> ch_;
        if(ch_ == 'S') {
            q[i].opt = 1; q[i].x = read(); q[i].l = read() + 1; q[i].r = read();
        }
        if(ch_ == 'A') {
            q[i].opt = 2; q[i].l = read(); q[i].r = read();
        }
    }
    for(int i = 1;i < n - 1; i++) {
        calc(i);
        // cout << i << ":" << ans[i] << " " << len[i] << "\n";
    }
    printf("%d\n", calc(n - 1));

    fclose(stdin); fclose(stdout);

    return 0;
}

T2

你有 n 株植物,高度为 \(a_i\)​。你希望让它们的高度看起来不太相同,具体来说:任意两株高度差的绝对值不小于 d。

每次你可以选择一株植物,将其拔高或压低 1,这花费你 1 的力气,这个操作可以进行任意多次。注意,植物高度不能是负的,即你需要保证任意时刻 \(a_i\geq 0\)

你最少需要使用多少力气,才能使得植物高度看起来不太相同呢?

多组测试数据。第一行一个整数 T 为数据组数,接下来每组格式如下:

第一行两个整数 n,d。

接下来一行 n 个整数 \(a_i\)​。

对于全部数据,\(\sum n\leq 3\cdot 10^5,1\leq d\leq 10^6, 0\leq a_i\leq 10^{11}\)

开始想了了费用流的做法, 骗了60pts.

就是先给每个\(a_i\)排序, 然后求出所有 \(x = a_{i+1} - a_i - d\), 对于\(x\)小于0的就从源点向他连一条流量为\(abs(x)\), 费用为0的边, 对于大于0的就从他向汇点连一条流量为\(x\), 费用为0的边. 然后这些点之间都连双向的流量为\(inf\), 费用为1的边. 为啥呢?

如果说提高一个\(i\)的高度的话, \(i-1\)\(i\)的高度差就会增加1, \(i+1\)\(i\)的高度差就会减小1, 然后又花了1的体力. 都减\(d\)是因为要保证相邻两个的差值大于等于d.

但是还有特殊的两个节点, 就是最小的那个可以压到0, 最大的那个可以无限高, 于是再连两条特殊的边就好了 : 从0向汇点连一条流量为\(a_1\), 费用为0的边, 从\(n+1\)向汇点连一条流量为\(inf\), 费用为0的边.

#include <bits/stdc++.h>

using namespace std;

inline long long read() {
    long long s = 0, f = 1; char ch;
    while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
    for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
    return s * f;
}

const int N = 4e5, M = 3e6;
const long long inf = 1e18, Inf = 1e9;
int s, t, n, d_, cnt;
int d[N], pre[N], las[N], head[N];
long long b[N], a[N], incf[N];
struct edge { int v, to, nxt; long long c; } e[M];

void add(int x, int y, long long z1, int z2) {
    // cout << x << " " << y << " " << z1 << " " << z2 << "!!!!\n";
    e[++ cnt].nxt = head[x]; head[x] = cnt; e[cnt].to = y; e[cnt].c = z1; e[cnt].v = z2;
    e[++ cnt].nxt = head[y]; head[y] = cnt; e[cnt].to = x; e[cnt].c = 0; e[cnt].v = -z2;
}

int bfs() {
    for(int i = s;i <= t; i++) d[i] = Inf, incf[i] = inf;
    queue <int> q; d[s] = 0; pre[t] = -1; q.push(s);
    while(q.size()) {
        int x = q.front(); q.pop();
        for(int i = head[x]; i ; i = e[i].nxt) {
            int y = e[i].to;
            if(e[i].c && d[y] > d[x] + e[i].v) {
                d[y] = d[x] + e[i].v; q.push(y);
                incf[y] = min(incf[x], e[i].c);
                pre[y] = x; las[y] = i;
            }
        }
    }
    return pre[t] != -1;
}

void Work() {
    long long res = 0;
    while(bfs()) {
        int now = t;
        res += 1ll * d[t] * incf[t];
        // cout << d[t] << " " << incf[t] << "!!!!!\n";
        while(now != s) {
            e[las[now]].c -= incf[t];
            e[las[now] ^ 1].c += incf[t];
            now = pre[now];
        }
    }
    printf("%lld\n", res);
}

void Clear() {
    cnt = 1; s = 0; t = n + 2;
    memset(head, 0, sizeof(head));
}

void Init() {
    n = read(); d_ = read();
    Clear();
    for(int i = 1;i <= n; i++) a[i] = read();
    sort(a + 1, a + n + 1);
    for(int i = 2;i <= n; i++) b[i] = a[i] - a[i - 1] - d_; 
    add(1, t, a[1], 0); 
    for(int i = 2;i <= n; i++) 
        if(b[i] < 0) add(s, i, -b[i], 0);
        else add(i, t, b[i], 0);
    add(n + 1, t, inf, 0);
    for(int i = 1;i <= n + 1; i++) {
        if(i != 1) add(i, i - 1, inf, 1);
        if(i != n + 1) add(i, i + 1, inf, 1);
    }
}

int main() {

    freopen("different.in","r",stdin); freopen("different.out","w",stdout);

    for(int T = read(); T ; T --) {
        Init(); Work();
    }

    fclose(stdin); fclose(stdout);

    return 0;
}

/*
2
4 10
1 100 7 10
5 1
1 1 1 1 1

1
5 1
1 1 1 1 1
*/

接下来说说正解吧.

正解是DP.其实我知道该用DP写这道题, 但是我不会qwq.

首先有一个经典的转化 : \(A_i = a_i - (i-1)*d\). 那么问题就转化成了将\(A_i\)序列变成单调不降序列的最小代价.(注意\(a_i\)是排过序的)

\(f_{i,j}\)表示前\(i\)个元素转化成单调不降序列, 并且\(A_i = j\)的最小代价.那么可以列出转移方程 :

\[ f_{i,j} = |A_i - j| +min\{ f_{i-1, -\infty - j} \} \]

怎么优化呢? 我们可以考虑一下它的函数图像是什么, 我们如果可以找到它的最低点就好了.

先画一下\(f_{1,j} = |A_1 - j|\)的图像 : 就是普通的绝对值函数图像, 没有啥好画的.

然后在考虑一下\(f_{2,j} = |A_2 - j| + min\{ f_{1, -\infty-j} \}\)的图像是什么, 就是一段绝对值函数和刚刚\(f_1\)的前缀最小值函数合并一下, 类似一个下凸壳.

最后\(f_{n,j}\)的图像也是一个下凸壳, 并且斜率是连续的, 0, -1, -2, \(\dots\), -k.

最后的答案就是\(min\{ f_{n,j} \}\), 我们需要找到函数图像最低的点在哪里.

\(s_i\)是下凸壳上定点的横坐标, 并且从大到小排序.

那么最后的答案就是\(f_{n,0} - \sum_{i=1}^{n-1} i * (s_i-s_{i-1})\), \(i\)就是那个斜率嘛, 是连续的.

现在就考虑\(f_{n_0}\)怎么求就好了. 因为要求\(f_{n,0}\), 所以\(A_n = 0\), 又因为\(A_1 = a_1 \geq 0\), 所以整个\(A_i\)序列一定都是0, 因为要保证单调不降, 那么很自然地, \(f_{n,0} = \sum_{i=1}^{n} A_i\).

具体的维护这个图像的拐点要用到堆, 看下代码就好了.

#include <bits/stdc++.h>

using namespace std;

inline long long read() {
    long long s = 0, f = 1; char ch;
    while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
    for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
    return s * f;
}

const int N = 3e5 + 5;
long long a[N], s[N];
priority_queue <long long> q;

int main() {

    for(int T = read(); T ; T --) {
        int n = read(), d = read(); 
        for(int i = 1;i <= n; i++) a[i] = read();
        sort(a + 1, a + n + 1);
        for(int i = 1;i <= n; i++) {
            long long x = max(0ll, a[i] - 1ll * (i - 1) * d);
            q.push(x); q.push(x); q.pop();
        }
        int cnt = 0;
        long long res = 0;
        for(int i = 1;i <= n; i++) res += abs(a[i] - 1ll * (i - 1) * d);
        while(q.size()) s[++ cnt] = q.top(), q.pop(); s[++ cnt] = 0;
        for(int i = 1;i < cnt; i++) res -= (s[i] - s[i + 1]) * i;
        printf("%lld\n", res);
    }

    return 0;
}

T3

你有 2n+m 根绳子,每根绳子两端染有黑、白两种颜色。

其中 n 根绳子两端都是黑色; n 根绳子两端都是白色; m 根绳子一段黑色另一端白色。

现在绳子一端为黑色和白色的线头分别有 2n+m 个,你想要把黑色和白色一端的线头随机配对(共 (2n+m)! 种方法),每对配对的线头用胶水拼接起来。可以发现,这样所有绳子一定会绕成若干个环。

你想要求出环的个数期望是多少,结果对 \(10^9+7\) 取模。

一行两个整数 n,m。

对于全部数据,\(0\leq n,m\leq 10^6\)

一看期望就头疼啊md.

直接设\(f_{n,m}\)是两端都为0的线段有\(n\)条, 两端都为1的线段有\(m\)条, 一端为0一端为1的有\(m\)条的期望答案是多少. 最后输出\(f_{n,m}\)就好了.

然后分情况讨论, 我们拿出一条一端是0, 一端是1的来和别的组合 :

1.和两端都是0或两端都是1的连接, 那么连完之后会得到两端都是1或两端都是0的(注意顺序), 并没有形成环, 这样的一共有\(2n\)种方案.

2.和一端为1一端为0的连(不包括自己), 连完之后会得到一端为0一端为1的, 并没有形成环, 这样的一共有\(m-1\)种方案.

3.自己首尾相连, 连完之后会得到一个环, 这样的一共有\(1\)种方案.

那么总共有\(2n+m\)中方案, 有贡献的只有1个, 那么可以得到转移方程 :

\[f_{n,m} = f_{n,m-1} + \frac{1}{2n+m} \]

如果说我们知道了\(f_{n,0}\)的话就可以\(O(n)\)求答案了.

两端为0的只能和两端为1的去连, 因为没有一端是0一端是1的了, 再结合上面那个式子就可以得到 :

\[f_{n,0} = f_{n-1,1} = f_{n-1,0} + \frac{1}{2n-1} \]

也是可以\(O(n)\)求的.

代码非常短 :

#include <bits/stdc++.h>

using namespace std;

inline long long read() {
    long long s = 0, f = 1; char ch;
    while(!isdigit(ch = getchar())) (ch == '-') && (f = -f);
    for(s = ch ^ 48;isdigit(ch = getchar()); s = (s << 1) + (s << 3) + (ch ^ 48));
    return s * f;
}

const int N = 1e6 + 5, mod = 1e9 + 7;
int n, m, ans;

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

int main() {

    n = read(); m = read();
    for(int i = 0;i < n; i++) ans = (ans + ksm(2 * i + 1, mod - 2)) % mod;
    for(int i = 1;i <= m; i++) ans = (ans + ksm(2 * n + i, mod - 2)) % mod;
    printf("%d", ans);

    return 0;
}
posted @ 2021-03-15 20:42  C锥  阅读(79)  评论(0编辑  收藏  举报