一本通课后练习 / 高手训练

扑克牌

/贪心/堆/二分查找/

题目链接 一本通

题目链接 洛谷

Solution

可以看到洛谷和一本通上的数据范围有微妙的区别...其中洛谷的算法(算法二)是通用算法,而一本通的算法(算法一)只能过它自己的数据。

算法一

每次贪心地把 joker 给当前排数最少的堆,直到没有 joker 或有牌用完,优先队列维护。设排数为 m,时间复杂度约 O(mlogm)。

算法二

首先二分,当前答案为 mid。把 joker 看成一堆牌,问题变成了从 n+1 堆牌里面取出 n 个,能否取 mid 次。枚举 1-n,若 (a_i)<mid,统计 (a_i) 与 mid 的差值和 res,这些需要用 joker 补上。若 res <= min(x,m) 则 return true;否则 return false。

Code

算法一 Code

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;

const int N = 2333333;
int n, m, a[N], cnt = 0;
priority_queue <int> q;


int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++)
        scanf("%d", &a[i]), q.push(-a[i]);
    while(!q.empty())
    {
        int a = -q.top();
        q.pop();
        if(cnt < m) a++;
        q.push(-a);
        if(-q.top() - cnt <= 0) break;
        cnt++;
    }
    printf("%d", cnt);
    return 0;
}

算法二 Code

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

long long n, m, a[23333], l = 0, r = 1e9;

bool check(long long x)
{
    long long res = 0;
    for(int i = 1; i <= n; i++)
        if(a[i] - x < 0) res += a[i] - x;
    return (-res) <= min(m, x);
}

int main()
{
    scanf("%lld%lld", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%lld", &a[i]);
    while(l + 1 < r)
    {
        long long mid = (l + r) >> 1;
        if(check(mid)) l = mid;
        else r = mid;
    }
    printf("%lld", l);
    return 0;
}

楼间跳跃

题目链接

Solution

好不容易做出来一题 QwQ

这题跟钓鱼那题很像...最优情况下肯定不会从 i+1 号楼走到 i 号楼(即不会往回走),考虑枚举最远到达的楼号 i,那么首先要走 i 步,≤i 的楼都要拿到 h[i] 的价值,可以前缀和维护。经过手玩发现一个神奇的性质,即在同一栋楼 i 中通过走路和坐滑梯,可以在这栋楼中到达任意的 k 层,并且到达时当前楼层中一定能拿到价值(不会浪费时间),然后到达 i+1 号楼。

所以可以用优先队列来维护前面 m-i 个最大值之和作为额外价值。由于数据范围很大,需要整栋楼地入队和出队。时间复杂度约为 O(nlogn)。

Code

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
using namespace std;

const int N = 2333333;
struct node
{
    LL w, siz;
    friend bool operator < (node x, node y)
    {
        return x.w > y.w;
    }
};
LL n, m, p, Ans = 0, Num = 0, Size = 0, h[N], v[N], sum[N];
priority_queue <node> q;

inline LL read()
{
    char ch = getchar(); LL x = 0, f = 1;
    while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
    while(ch >= '0' && ch <= '9') { x = x * 10 + ch - 48; ch = getchar(); }
    return f * x;
}

int main()
{
    n = read(), m = read();
    sum[0] = 0, m++;
    for(int i = 1; i <= n; i++) h[i] = read(), v[i] = read();
    for(int i = 1; i <= n; i++) sum[i] = sum[i - 1] + v[i];
    for(int i = 1; i <= n; i++)
    {
        if(m - i < 0) break;
        q.push((node) { v[i], h[i] - 1 });
        Num += v[i] * (h[i] - 1);
        Size += h[i] - 1, p = Size - (m - i);
        while(p > 0)
        {
            node x = q.top();
            q.pop();
            if(p >= x.siz) Size -= x.siz, Num -= (x.siz * x.w), p -= x.siz;
            else
            {
                Size -= p;
                q.push((node) { x.w, x.siz - p });
                Num -= p * x.w;
                p = 0;
            }
        }
        Ans = max(Ans, sum[i] + Num);
    }
    printf("%lld", Ans);
    return 0;
}

SCOI2010 传送带

/三分/

题目链接

Solution

可以把走法抽象成下面的图:

A-E-F-D,其中 E 在线段 AB 上,F 在线段 CD 上(均可与端点重合)。

不难看出 E 的位置作为自变量,与时间的关系是一个二次函数,而对于每一个 E,F 的位置又是一个二次函数。所以基本思路为三分套三分,其中 E、F 点坐标的求解等需要用到相似三角形和一次函数的部分知识。其他的一些细节在代码里。

Code

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;

double ax, ay, bx, by, cx, cy, dx, dy, P, Q, R, eps = 1e-6;

double dis(double x1, double y1, double x2, double y2)
{
    return (double)(sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)));
}

double f(double x)
{
    double ex, ey;
    if(dis(ax, ay, bx, by) == 0) ex = ey = 0;
    else
    {
        ex = fabs(ax - bx) * (x / dis(ax, ay, bx, by));
        ey = fabs(ay - by) * (x / dis(ax, ay, bx, by));
    }
    ex = max(ex, 0.0), ey = max(ey, 0.0);
    if(ax < bx) ex = ax + ex;
    else ex = ax - ex;
    if(ay < by) ey = ay + ey;
    else ey = ay - ey;
    double l = 0, r = dis(cx, cy, dx, dy), res;
    do
    {
        double m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
        double fx1, fy1, fx2, fy2;
        if(dis(cx, cy, dx, dy) == 0) fx1 = fx2 = fy1 = fy2 = 0;
        else
        {
            fx1 = fabs(cx - dx) * (m1 / dis(cx, cy, dx, dy));
            fy1 = fabs(cy - dy) * (m1 / dis(cx, cy, dx, dy));
            fx2 = fabs(cx - dx) * (m2 / dis(cx, cy, dx, dy));
            fy2 = fabs(cy - dy) * (m2 / dis(cx, cy, dx, dy));
        }
        fx1 = max(fx1, 0.0), fy1 = max(fy1, 0.0);
        fx2 = max(fx2, 0.0), fy2 = max(fy2, 0.0);
        if(cx < dx) fx1 = dx - fx1, fx2 = dx - fx2;
        else fx1 = dx + fx1, fx2 = dx + fx2;
        if(cy < dy) fy1 = dy - fy1, fy2 = dy - fy2;
        else fy1 = dy + fy1, fy2 = dy + fy2;
        double len1 = dis(ex, ey, fx1, fy1);
        double len2 = dis(ex, ey, fx2, fy2);
        double F1 = x / P + m1 / Q + len1 / R;
        double F2 = x / P + m2 / Q + len2 / R;
        if(F1 < F2) r = m2;
        else l = m1;
        res = F1;
    } while(r - l >= eps);
    return res;
}

int main()
{
    cin >> ax >> ay >> bx >> by >> cx >> cy >> dx >> dy;
    cin >> P >> Q >> R;
    double l = 0, r = dis(ax, ay, bx, by);
    do
    {
        double m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
        if(f(m1) < f(m2)) r = m2;
        else l = m1;
    } while(r - l >= eps);
    printf("%.2lf", f(l));
    return 0;
}

独木桥

/二分/

题目链接

Solution

可以先看一下洛谷上的独木桥。其中有一个重要的思路:如果把每一个人看成没有区别的点,两个人相遇然后各自转头,相当于他们互相穿过了对方。

于是我们可以通过初始向左的位置 -T,初始向右的位置 +T算出 T 时间后小孩位置构成的集合 S。

但是这个做法不能分清具体的小孩,这就需要用到本题的另一个性质:孩子的相对位置不会发生改变。即一个孩子 i,初始时若按照位置由小到大排序,他的排名为 (rank_i),则 T 时刻后他的排名依然为 (rank_i)。因此,问题转化为在集合 S 中求出第 k 小的数。

对于每一次询问二分答案,左边右边分开排序,统计向左的集合中 pos<mid+T 和向右的集合中 pos<mid-T 的个数,求出当前排名 rk(这一过程还是可以二分)。若 rk>k,r=mid;否则 l=mid。时间复杂度约为 O(nlogn)

Code

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;

const int N = 2333333, LEN = 1e10 + 2333;
struct Child { int p, d, id; } c[N];
int n, l, r, c1 = 0, c2 = 0, K, T, Q, a[N], b[N], rk[N];

int check(int x, int T)
{
    int l1 = 0, r1 = c1, a1;
    while(l1 + 1 < r1)
    {
        int mid = (l1 + r1) >> 1;
        if(a[mid] < x + T) l1 = mid;
        else r1 = mid;
    }
    if(a[r1] < x + T) a1 = r1;
    else a1 = l1;
    
    int l2 = 0, r2 = c2, a2;
    while(l2 + 1 < r2)
    {
        int mid = (l2 + r2) >> 1;
        if(b[mid] < x - T) l2 = mid;
        else r2 = mid;
    }
    if(b[r2] < x - T) a2 = r2;
    else a2 = l2;
    return a1 + a2 + 1;
}

bool cmp(Child x, Child y) { return x.p < y.p; }

signed main()
{
    scanf("%lld", &n);
    for(int i = 1; i <= n; i++) scanf("%lld", &c[i].p), c[i].id = i;
    for(int i = 1; i <= n; i++)
    {
        scanf("%lld", &c[i].d);
        if(c[i].d == 0) a[++c1] = c[i].p;
        if(c[i].d == 1) b[++c2] = c[i].p;
    }
    sort(a + 1, a + c1 + 1);
    sort(b + 1, b + c2 + 1);
    sort(c + 1, c + n + 1, cmp);
    for(int i = 1; i <= n; i++) rk[c[i].id] = i;
    scanf("%lld", &Q);
    while(Q--)
    {
        scanf("%lld%lld", &K, &T);
        l = -LEN, r = LEN;
        K = rk[K + 1];
        while(l + 1 < r)
        {
            int mid = (l + r) >> 1;
            if(check(mid, T) > K) r = mid;
            else l = mid;
        }
        if(check(r, T) == K) printf("%lld
", r);
        else printf("%lld
", l);
    }
    return 0;
}

子集

/二分/

题目链接

Solution

因为元素的位置对答案没有影响,先对 a 数组进行排序。

子集的中位数有两种情况:

若子集大小为 2k,则中位数为 (a_k)

若子集大小为 2k+1,则中位数为 (frac{a_k+a_{k+1}}{2})

根据题面,总结出以下两条结论:

1.价值最大的子集的价值 ≥0(ans 的初值为 0)

2.子集大小一定为奇数。可以证明若子集大小为 2k,则删掉 (a_{k+1}) 一定不劣(证明在书上,看不懂 QwQ。但是可以猜想后对拍验证)

首先 O(n) 枚举中位数 x,二分 k,表示从 x 左边选出 k 个,右边选出 k 个(肯定贪心选最大的,和可以用前缀/后缀和维护)。设 check(i) 表示选择 i 的最终答案,若 check(k)<check(k-1),显然若 k 继续增大答案一定不会变优,此时 r=mid;否则 l=mid。

Code

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 233333;
int n, a[N], sum[N];

double Ans = 0.0;

double get(int x, int k)
{
    double a1 = sum[x - k], a2 = sum[x + 1], a3 = sum[n - k + 1];
    return (double)(a1 - a2 + a3) / (2 * (k * 1.0) + 1.0);
}

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
    sort(a + 1, a + n + 1);
    sum[n + 1] = 0;
    for(int i = n; i >= 1; i--) sum[i] = sum[i + 1] + a[i];
    for(int i = 2; i <= n; i++)
    {
        double x = a[i];
        int l = 1, r = min(n - i, i - 1);
        while(l + 1 < r)
        {
            int mid = (l + r) >> 1;
            if(get(i, mid) < get(i, mid - 1)) r = mid;
            else l = mid;
        }
        Ans = max(Ans, max(get(i, l) - x, get(i, r) - x));
    }
    printf("%.5lf", Ans);
    return 0;
}

friends

/字符串哈希/

题目链接

Solution

注意书中计算一段连续区间 (C' = c_{k+1}, c_{k+2}, ..., c_{k+n}) 的哈希值公式为 (H(C')=H(C, k+n)-H(C, k) * b^n)。考虑枚举插入的字符 (x),它一定把 2 个 S 中的其中一个拆成了两半,这时候我们需要根据公式把它拼起来。设前一段为 S1,长度为 l1,后一段为 S2,长度为 l2,则该段拼合后的哈希值为 (H(S1)*b^{l2}+H(S2))

计算和讨论过程相对复杂,注意,输出 NOT UNIQUE 当且仅当存在两个不一样的 S,而不是存在两个不一样的 x 或存在两个不一样的 posx

Code

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;

const int N = 2333333;
const long long h1 = 1e9 + 7, h2 = 1e9 + 9, b = 131;
long long n, len, pos = -1, ans1, ans2;
long long a[N], b1[N], b2[N], s1[N], s2[N];
string s, res = "";

long long get1(long long l, long long r)
{
    if(r < l) return 0;
    return ((s1[r] - (s1[l - 1] * b1[r - l + 1]) % h1) % h1 + h1) % h1;
}

long long get2(long long l, long long r)
{
    if(r < l) return 0;
    return ((s2[r] - (s2[l - 1] * b2[r - l + 1]) % h2) % h2 + h2) % h2;
}

int main()
{
    cin >> n >> s;
    if(n % 2 == 0)
    {
        printf("NOT POSSIBLE");
        return 0;
    }
    len = (n - 1) / 2;
    for(int i = 0; i < n; i++) a[i + 1] = s[i];
    b1[0] = b2[0] = 1, s1[0] = s2[0] = 0;
    for(int i = 1; i <= n; i++)
        b1[i] = (b1[i - 1] * b) % h1,
        b2[i] = (b2[i - 1] * b) % h2;
    for(int i = 1; i <= n; i++)
        s1[i] = (s1[i - 1] * b + a[i]) % h1,
        s2[i] = (s2[i - 1] * b + a[i]) % h2;
    for(int i = 1; i <= n; i++)
    {
        long long num1l, num2l, num1r, num2r;
        if(i - 1 < len)
        {
            int tot = len + 1 - i;
            num1l = (get1(i + 1, len + 1) + (get1(1, i - 1) * b1[len - i + 1]) % h1) % h1;
            num2l = (get2(i + 1, len + 1) + (get2(1, i - 1) * b2[len - i + 1]) % h2) % h2;
            num1r = get1(n - len + 1, n);
            num2r = get2(n - len + 1, n);
        }
        else if(i - 1 > len)
        {
            num1l = get1(1, len);
            num2l = get2(1, len);
            num1r = (get1(i + 1, n) + (get1(n - len, i - 1) * b1[n - i]) % h1) % h1;
            num2r = (get2(i + 1, n) + (get2(n - len, i - 1) * b2[n - i]) % h2) % h2;
        }
        else if(i - 1 == len)
        {
            num1l = get1(1, len);
            num2l = get2(1, len);
            num1r = get1(n - len + 1, n);
            num2r = get2(n - len + 1, n);
        }
        if(num1l == num1r && num2l == num2r)
        {
            if(pos != -1 && (ans1 != num1l || ans2 != num2l))
            {
                printf("NOT UNIQUE");
                return 0;
            }
            pos = i;
            ans1 = num1l, ans2 = num2l;
        }
    }
    if(pos == -1)
    {
        printf("NOT POSSIBLE");
        return 0;
    }
    if(pos - 1 < len)
    {
        for(int i = 1; i < pos; i++) res += a[i];
        for(int i = pos + 1; i <= len + 1; i++) res += a[i];
    }
    else for(int i = 1; i <= len; i++) res += a[i];
    cout << res;
    return 0;
}

A Horrible Poem

题目链接

参考题解

Solution

数据范围非常不友好...首先,x 是区间 ([l,r]) 的一个循环节的条件是:

  1. x 是 r-l+1 的一个因数

  2. ([l,r]) 去掉前 x 个字符和 ([l,r]) 去掉后 x 个字符的哈希值相等,即 (H_{l+x,r}=H_{l,r-x})

如果暴力枚举因数,根号的复杂度会超时,考虑优化:

设最小循环节长度为 len,则原区间长度 n = len * k。将 k 的质因数依次分解,每次试除 k,得到的 k' 和 len 的乘积仍是一个循环节。利用这个性质,依次用质数 i 试除 n,若除去后仍是循环节,说明 i 是 k 的一部分。将其除去,留下的就是 len。

Solution

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#define int long long
using namespace std;

const int N = 2333333, h1 = 1e9 + 7, h2 = 1e9 + 9, b = 131;
int n, Q, A, B, cnt = 0, ans, tmp;
int b1[N], b2[N], s1[N], s2[N], minp[N], p[N];
string s;

void Prim()
{
    memset(minp, 0, sizeof(minp));
    for(int i = 2; i <= n; i++)
    {
        if(!minp[i]) p[++cnt] = i, minp[i] = i;
        for(int j = 1; j <= cnt; j++)
        {
            if(i * p[j] > n || p[j] > minp[i]) break;
            minp[i * p[j]] = p[j];
        }
    }
}

int get1(int l, int r)
{
    return ((s1[r] - s1[l - 1] * b1[r - l + 1]) % h1 + h1) % h1;
}
int get2(int l, int r)
{
    return ((s2[r] - s2[l - 1] * b2[r - l + 1]) % h2 + h2) % h2;
}
bool check(int l, int r, int x)
{
    return (get1(l + x, r) == get1(l, r - x)) && (get2(l + x, r) == get2(l, r - x));
}

signed main()
{
    cin >> n >> s;
    Prim();
    b1[0] = b2[0] = 1;
    for(int i = 1; i <= n; i++) b1[i] = (b1[i - 1] * b) % h1;
    for(int i = 1; i <= n; i++) b2[i] = (b2[i - 1] * b) % h2;
    s1[0] = s2[0] = 0;
    for(int i = 1; i <= n; i++) s1[i] = (s1[i - 1] * b + s[i - 1]) % h1;
    for(int i = 1; i <= n; i++) s2[i] = (s2[i - 1] * b + s[i - 1]) % h2;
    scanf("%lld", &Q);
    while(Q--)
    {
        scanf("%lld%lld", &A, &B);
        ans = tmp = B - A + 1;
        while(tmp != 1)
        {
            int t = minp[tmp];
            while(tmp % t == 0 && check(A, B, ans / t)) ans /= t, tmp /= t;
            while(tmp % t == 0) tmp /= t;
        }
        printf("%lld
", ans);
    }
    return 0;
}
原文地址:https://www.cnblogs.com/Andy-park/p/13569307.html