牛客多校第五场7月31日补题记录

B Boxes

题意:有 n n n 个盒子,每个盒子中装有黑球白球概率均为 1 2 \displaystyle \frac{1}{2} 21。打开第 i i i 个盒子所需代价为 w i w_i wi。现在有一个机会,使用 c c c 的代价知晓剩余盒子中黑球个数,问使用最优策略开盒子直到直到全部盒子装球颜色情况的期望最小代价。

解法:显然询问盒子中有几个黑球这个策略一定只会用一次:第二次用的答案一定可以通过第一次询问的结果与中间新开出来的结果得到。并且越早用越好,因而在 ∑ w i < c \sum w_i<c wi<c 的情况下一定是先用 c c c 的代价去询问。

由于每个盒子黑白球概率相同,因而一定是先开权值小的盒子。按照权值排序,从小到大的开盒子。接下来考虑第 i i i 个盒子在什么时候会打开。

记一开始的询问结果为 x x x。开盒子的终止条件,可以是摸到了 x x x 个黑球,也可以是摸到了 n − x n-x nx 个白球——但是有一点是共同的,那就是在这个盒子之后,剩余球的颜色全部相同,要么全黑要么全白。考虑第 i i i 个盒子,如果没有开到这个盒子,证明前面就已经停了。恰好在第 i i i 位停止的概率为 P ( i ) = 1 2 n − i − 1 , i < n P(i)=\displaystyle \frac{1}{2^{n-i-1}},i<n P(i)=2ni11,i<n,显然最后一个盒子一定是不会开的。那么打开第 i i i 位盒子的概率为 1 − ∑ j = 1 i − 1 P ( j ) = 1 − 1 2 n − i \displaystyle 1-\sum_{j=1}^{i-1}P(j)=1-\frac{1}{2^{n-i}} 1j=1i1P(j)=12ni1。因而期望概率为 ∑ i = 1 n − 1 w i ( 1 − 1 2 n − i ) \displaystyle \sum_{i=1}^{n-1} w_i (1-\frac{1}{2^{n-i}}) i=1n1wi(12ni1)。注意精度问题,使用 long double 可以通过。

#include <cstdio>
#include <algorithm>
#include <iostream>
using namespace std;
long double w[100005], sum[100005];
int main()
{
    int n;
    long double c;
    scanf("%d%Lf", &n, &c);
    for (int i = 1; i <= n; i++)
        cin >> w[i];
    sort(w + 1, w + n + 1);
    for (int i = 1; i <= n; i++)
        sum[i] = sum[i - 1] + w[i];
    long double ans = 0, p = 1;
    for (int i = n; i >= 1;i--)
    {
        ans += (1 - p) * w[i];
        p /= 2;
    }
    printf("%.10Lf", min(ans + c, sum[n]));
    return 0;
}

C Cheating and Stealing

题意:给定 n n n 局乒乓球赛的结果,求出当赛点为 i ∈ [ 1 , n ] i \in [1,n] i[1,n] 时的比赛结果。 n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105

解法:容易发现,当赛点为 x x x 时,总体局数仅 O ( n x ) \displaystyle O(\frac{n}{x}) O(xn) 局,根据调和级数的性质, ∑ i = 1 n O ( n x ) = O ( n log ⁡ n ) \displaystyle \sum_{i=1}^{n} O(\frac{n}{x})=O(n \log n) i=1nO(xn)=O(nlogn),因而只要每一局能用 O ( 1 ) O(1) O(1) 时间计算即可。

考虑如何快速的找到一局的结束。以第一局为例,此时赛点为 x x x。后面的局的分数判定可以使用前缀和将前面的局数分数挖掉。

首先找到二人达到 x x x 分时的位置,取小者。此处可以使用桶进行存储,快速找到多少分在哪一场,与哪一场现在积累多少分。不妨令 PJ King 先拿到了 x x x 分,在第 y y y 场。如果此时 PJ King 在第 y y y 场拿到的分数不足 x − 2 x-2 x2 分,则本局结束。

显然此时二人分数不可能相同,并且 PJ King 领先。如果下一局还是他赢了,那么本局也结束了。否则,二人此时分数相同。那么接下来可能产生拉锯战——交错赢球,时刻平局。

引入第二个数组—— T i e b r e a k \rm Tiebreak Tiebreak。这个数组表示了当前一人领先的情况下该人获胜的场次。只需要一个递推即可求出该数组:

	tiebreak[n] = tiebreak[n + 1] = n + 1;
    for (int i = n - 1; i >= 1;i--)
        tiebreak[i] = a[i] == a[i + 1] ? i + 1 : tiebreak[i + 2];

那么我们现在可以快速的找到终止这种平局的位置了—— y = T i e b r e a k [ y + 1 ] y= {\rm Tiebreak}[y+1] y=Tiebreak[y+1]。 因为第 y y y 局时是平局,所以要加一。那么此时二人分数已定,本局结束,统计结果。

整体复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const long long mod = 998244353;
int tiebreak[2000005], sumw[2000005], suml[2000005], posw[2000005], posl[2000005];
//posl是PJ King 在x分时对应场数,posw同理
char a[2000005];
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n * 2;i++)
        posw[i] = posl[i] = n + 1;
    scanf("%s", a + 1);
    for (int i = 1; i <= n; i++)
    {
        sumw[i] = sumw[i - 1];
        suml[i] = suml[i - 1];
        if (a[i] == 'W')
        {
            sumw[i]++;
            posw[sumw[i]] = i;
        }
        else
        {
            suml[i]++;
            posl[suml[i]] = i;
        }
    }
    tiebreak[n] = tiebreak[n + 1] = n + 1;
    for (int i = n - 1; i >= 1;i--)
        tiebreak[i] = a[i] == a[i + 1] ? i + 1 : tiebreak[i + 2];
    long long ans = 0, x = 1;
    for (int i = 1; i <= n;i++)
    {
        int res = 0;
        int old = 0, place = 0;
        while (old < n)
        {
            int Wwin = posw[sumw[old] + i];
            int Lwin = posl[suml[old] + i];
            place = min(Wwin, Lwin);
            if(place>n)
                break;
            if (abs((sumw[place] - sumw[old]) - (suml[place] - suml[old])) >= 2)
            {
                if (sumw[place] - sumw[old] > suml[place] - suml[old])
                    res++;
                old = place;
                continue;
            }
            place++;
            if (place > n)
                break;
            if (abs((sumw[place] - sumw[old]) - (suml[place] - suml[old])) >= 2)
            {
                if (sumw[place] - sumw[old] > suml[place] - suml[old])
                    res++;
                old = place;
                continue;
            }
            place = tiebreak[place + 1];
            if(place>n)
                break;
            if (sumw[place] - sumw[old] > suml[place] - suml[old])
                res++;
            old = place;
        }
        ans = (ans + res * x % mod) % mod;
        x = x * (n + 1) % mod;
    }
    printf("%lld", ans);
    return 0;
}

D Double strings

题意:给定长度为 n n n m m m 的两个字符串 S , T S,T S,T,问分别从 S , T S,T S,T 中选出长度相同的子串 a , b a,b a,b 且满足 a a a 的字典序小于 b b b 的方案数。 n , m ≤ 5 × 1 0 3 n,m \leq 5\times 10^3 n,m5×103

解法:将问题拆解成以下的几个子问题,

  1. 找到一段公共前缀
  2. 找到第一位不同的地方
  3. 找到后面任意长的等长子串

不妨令 f i , j f_{i,j} fi,j 表示 S S S 串到第 i i i 位, T T T 串到第 j j j 位的公共前缀取法总数。这个转移是非常好写的:

f i , j = { f i − 1 , j + f i , j − 1 , a i ≠ b j f i − 1 , j + f i , j − 1 + f i − 1 , j − 1 , a i = b j f_{i,j} = \begin{cases} f_{i-1,j}+f_{i,j-1}, & a_i\neq b_j \\[2ex] f_{i-1,j}+f_{i,j-1}+f_{i-1,j-1}, & a_i=b_j \end{cases} fi,j=fi1,j+fi,j1,fi1,j+fi,j1+fi1,j1,ai=bjai=bj

s u m f sumf sumf f f f 的二维前缀和,它表示了 S S S 串的前 i i i 位与 T T T 的前 j j j 位中取公共前缀的总方案数。则由上式与 s u m f i , j = s u m f i − 1 , j + s u m f i − 1 , j − s u m f i − 1 , j − 1 sumf_{i,j}=sumf_{i-1,j}+sumf_{i-1,j}-sumf_{i-1,j-1} sumfi,j=sumfi1,j+sumfi1,jsumfi1,j1 可以得到,

s u m f i , j = { s u m f i − 1 , j + s u m f i , j − 1 − s u m f i − 1 , j − 1 , a i ≠ b j s u m f i − 1 , j + s u m f i , j − 1 a i = b j sumf_{i,j} = \begin{cases} sumf_{i-1,j}+sumf_{i,j-1}-sumf_{i-1,j-1}, & a_i\neq b_j \\[2ex] sumf_{i-1,j}+sumf_{i,j-1} & a_i=b_j \end{cases} sumfi,j=sumfi1,j+sumfi,j1sumfi1,j1,sumfi1,j+sumfi,j1ai=bjai=bj

之后我们会用到 s u m f sumf sumf 进行答案的统计。

接下来是第三个问题。记 g i , j g_{i,j} gi,j S S S 串从第 i i i 位往后, T T T 串从第 j j j 位往后,满足 a i < b j a_i<b_j ai<bj,后面任取等长后缀的总方案数。它的计算公式为:

g i , j = { ( n + m − i − j n − i ) a i < b j 0 , a i ≥ b j g_{i,j} = \begin{cases} \dbinom{n+m-i-j}{n-i} & a_i< b_j \\[2ex] 0, & a_i \geq b_j \end{cases} gi,j=(nin+mij)0,ai<bjaibj

关于其公式的得到:其本质为在 n n n 个与 m m m 个物品中各选取 k k k 个物品方案的总和,即 ∑ k = 1 min ⁡ ( n , m ) ( n k ) ( m k ) = ( n + m n ) \displaystyle \sum_{k=1}^{\min(n,m)} \dbinom{n}{k} \dbinom{m}{k}=\dbinom{n+m}{n} k=1min(n,m)(kn)(km)=(nn+m)。此处可以使用 DP 求解,容易发现和杨辉三角的规律。

g g g 的二维后缀和 s u m g sumg sumg,表示了从 S S S 串的后面 i i i 位与 T T T 串的后面 j j j 位中选取的合法方案总和。

s u m g i , j = { s u m g i + 1 , j + s u m g i , j + 1 − s u m g i + 1 , j + 1 + ( n + m − i − j n − i ) a i < b j s u m g i + 1 , j + s u m g i , j + 1 − s u m g i + 1 , j + 1 , a i ≥ b j sumg_{i,j} = \begin{cases} sumg_{i+1,j}+sumg_{i,j+1}-sumg_{i+1,j+1}+\dbinom{n+m-i-j}{n-i} & a_i< b_j \\[2ex] sumg_{i+1,j}+sumg_{i,j+1}-sumg_{i+1,j+1}, & a_i \geq b_j \end{cases} sumgi,j=sumgi+1,j+sumgi,j+1sumgi+1,j+1+(nin+mij)sumgi+1,j+sumgi,j+1sumgi+1,j+1,ai<bjaibj

最后考虑第二步,只需要 O ( n m ) O(nm) O(nm) 的枚举这样的分段点即可。当 a i = b j a_i=b_j ai=bj 时,其方案总数为恰好在第 i − 1 , j − 1 i-1,j-1 i1,j1 位终止的方案数乘以其后缀的方案数,即 f i , j s u m g i + 1 , j + 1 f_{i,j} sumg_{i+1,j+1} fi,jsumgi+1,j+1

注意本题空间限制严格,不能开 long long。

总体复杂度 O ( n m ) O(nm) O(nm)

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int mod = 1000000007;
int power(int a,int x)
{
    int ans = 1;
    while(x)
    {
        if(x&1)
            ans = 1ll * ans * a % mod;
        a = 1ll * a * a % mod;
        x >>= 1;
    }
    return ans;
}
int inv(int x)
{
    return power(x, mod - 2);
}
int sumf[5005][5005], sumg[5005][5005];
int fac[10005], invfac[10005];
char a[5005], b[5005];
int C(int n,int m)
{
    if(m<0 || n<m)
        return 0;
    return (long long)fac[n] * invfac[m] % mod * invfac[n - m] % mod;
}
int main()
{
    fac[0] = 1;
    for (int i = 1; i <= 10000;i++)
        fac[i] = fac[i - 1] * (long long)i % mod;
    invfac[10000] = inv(fac[10000]);
    for (int i = 9999; i >= 1;i--)
        invfac[i] = invfac[i + 1] * (long long)(i + 1) % mod;
    invfac[0] = 1;
    scanf("%s", a + 1);
    scanf("%s", b + 1);
    int n = strlen(a + 1);
    int m = strlen(b + 1);
    sumf[0][0] = 1;
    for (int i = 1; i <= n;i++)
        sumf[i][0] = 1;
    for (int i = 1; i <= m;i++)
        sumf[0][i] = 1;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            if (a[i] == b[j])
                sumf[i][j] = (sumf[i - 1][j] + sumf[i][j - 1]) % mod;
            else
                sumf[i][j] = ((sumf[i - 1][j] + sumf[i][j - 1] - sumf[i - 1][j - 1]) % mod + mod) % mod;
    for (int i = n; i >= 1;i--)
        for (int j = m; j >= 1;j--)
        {
            sumg[i][j] = ((sumg[i][j + 1] + sumg[i + 1][j] - sumg[i + 1][j + 1]) % mod + mod) % mod;
            if(a[i]<b[j])
                sumg[i][j] = (sumg[i][j] + C(n + m - i - j, n - i)) % mod;
        }
    int ans = sumg[1][1];
    for (int i = 1; i <= n;i++)
        for (int j = 1; j <= m;j++)
            if(a[i]==b[j])
            {
                long long temp = (((long long)sumf[i][j] - sumf[i][j - 1] - sumf[i - 1][j] + sumf[i - 1][j - 1]) % mod + mod) % mod;
                ans = (ans + temp * sumg[i + 1][j + 1] % mod) % mod;
            }
    printf("%d", ans);
    return 0;
}

E Eert Esiwtib

题意:给定一个树,第 i i i 个点点权为 w i w_i wi,边上有运算符与、或、异或。 q q q 次互相独立的询问 ( d , u ) (d,u) (d,u),该次询问中将点权修改成 w i + d i w_i +di wi+di,问 u u u 节点的子树中全部节点到 u u u 路径上的权值按照边上给定的符号求出的权值的与、或、异或值。 d ≤ 100 d \leq 100 d100 n , q ≤ 1 × 1 0 5 n,q \leq 1\times 10^5 n,q1×105

解法:询问虽多,但是 d d d 很小,即本质不同的权值数不多,考虑分 d d d 进行询问的回答,因而对整个流程执行 100 100 100 次即可。

现在点的权值给定了。容易发现这是一个树形 DP, f u , 0 / 1 / 2 f_{u,0/1/2} fu,0/1/2 表示 u u u 的子树下全部节点到 u u u 路径上的权值的或、与、异或值。考虑合并操作,根据 u u u 到其儿子 v v v 边上的符号,分三种情况。

  1. ( u , v ) (u,v) (u,v) 边符号为或。
    再分三小点
    A. 或值
    考虑 v v v f v , 0 f_{v,0} fv,0 的构成,可以写作:
    ( w α 1 o p α 1 w α 2 o p α 2 ⋯   ) ∣ ( w β 1 o p β 1 w β 2 o p β 2 ⋯   ) ∣ ( w γ 1 o p γ 1 w γ 2 o p γ 2 ⋯   ) ∣ ⋯ (w_{\alpha_1} {\rm op_{\alpha_1} }w_{\alpha_2} {\rm op_{\alpha_2} } \cdots )|(w_{\beta_1} {\rm op_{\beta_1} }w_{\beta_2} {\rm op_{\beta_2} } \cdots )|(w_{\gamma_1} {\rm op_{\gamma_1} }w_{\gamma_2} {\rm op_{\gamma_2} } \cdots ) | \cdots (wα1opα1wα2opα2)(wβ1opβ1wβ2opβ2)(wγ1opγ1wγ2opγ2)
    那么 f u , 0 f_{u,0} fu,0 可以写作:
    ( w v ∣ w α 1 o p α 1 w α 2 o p α 2 ⋯   ) ∣ ( w v ∣ w β 1 o p β 1 w β 2 o p β 2 ⋯   ) ∣ ( w v ∣ w γ 1 o p γ 1 w γ 2 o p γ 2 ⋯   ) ∣ ⋯ (w_v|w_{\alpha_1} {\rm op_{\alpha_1} }w_{\alpha_2} {\rm op_{\alpha_2} } \cdots )|(w_v|w_{\beta_1} {\rm op_{\beta_1} }w_{\beta_2} {\rm op_{\beta_2} } \cdots )|(w_v|w_{\gamma_1} {\rm op_{\gamma_1} }w_{\gamma_2} {\rm op_{\gamma_2} } \cdots ) | \cdots (wvwα1opα1wα2opα2)(wvwβ1opβ1wβ2opβ2)(wvwγ1opγ1wγ2opγ2)
    但是这样分析是麻烦的,直接考虑每一位上的结果即可,下同。如果 w v w_v wv 上二进制位为 1 1 1,那么无论内部这些值或出来原来是多少,现在一定都是 1 1 1;如果当前 w v w_v wv 的二进制位上为 0 0 0,则直接看原来的结果。因而 f u , 0 = w v ∣ f v , 0 f_{u,0}=w_v|f_{v,0} fu,0=wvfv,0注意:这只是一个子树的合并结果,因而最终结果还要或上每一个子树,下同
    B.与值
    如果 w v w_v wv 当前位为 1 1 1,那么每一个后代节点(即最大的大括号内)中的值全部刷成 1 1 1,当前与值中一定具有 w v w_v wv 全部为 1 1 1 的二进制位;如果为 0 0 0,那就看之前的结果。因而 f u , 1 = w v & f v , 1 f_{u,1}=w_v\& f_{v,1} fu,1=wv&fv,1
    C.异或值
    异或值较为特殊,需要统计这一位上 1 1 1 出现的次数。如果子树大小为奇数,则 w v w_v wv 最后被异或了奇数次。 w v w_v wv 上为 0 0 0 的位显然是无所谓的,只考虑 1 1 1 的情况。显然之前无论这位是否为 1 1 1,呈现在这一步的运算中的全为 1 1 1,因而当前这一位有 1 1 1 的条件为子树大小为奇—— f u , 2 = w v ∣ f v , 2 f_{u,2}=w_v | f_{v,2} fu,2=wvfv,2;如果子树大小为为偶,则全部挖掉—— f u , 2 = ( w v ∣ f v , 2 ) ⊕ w v f_{u,2}=(w_v | f_{v,2}) \oplus w_v fu,2=(wvfv,2)wv

  2. ( u , v ) (u,v) (u,v) 符号为与
    A.或值
    w v w_v wv 上为 0 0 0 的位直接将每个括号里对应位全部刷成了 0 0 0,因而这些位或出来也是 0 0 0。如果 w v w_v wv 上二进位位为 1 1 1,则得看之前的结果。因而 f u , 0 = w v & f v , 0 f_{u,0} = w_v\& f_{v,0} fu,0=wv&fv,0
    B.与值
    同上,有 1 1 1 的位上才可能有贡献, f u , 1 = w v & f v , 1 f_{u,1}=w_v \& f_{v,1} fu,1=wv&fv,1
    C.异或值
    w v w_v wv 上为 0 0 0 的位直接退出讨论,剩余有 1 1 1 的位上,若括号内为 1 1 1 的次数为偶数次,那么现在与上之后,还是偶数次;之前为奇数次现在还是奇数次。因而 f u , 2 = w v & f v , 2 f_{u,2} = w_v \& f_{v,2} fu,2=wv&fv,2

  3. ( u , v ) (u,v) (u,v) 符号为异或
    A.或值
    w v w_v wv 二进制位为 0 0 0 的情况非常简单,直接承接此部分;如果为 1 1 1,则最后这一位上为 1 1 1 要看有没有这一位上存在 0 0 0 的位——只有这种才可能出现一个 1 1 1。因而去查询 f v , 1 f_{v,1} fv,1 来得到 f u , 0 f_{u,0} fu,0 的结果。
    B.与值
    同上,如果当前 w v w_v wv 这一位为 1 1 1,那么只有这一位之前全为 0 0 0 这一位才可能有 1 1 1
    C.异或值
    依旧分子树大小。此处可以考虑直接将 w v w_v wv 从每一个括号中拆出,如果拆出来奇数次则异或,否则不动。

整体复杂度 O ( n d ) O(nd) O(nd)

#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const long long inf = 0xffffffffffffffffll;
struct line
{
    int from;
    int to;
    int next;
    int op;
};
struct line que[200005];
long long w[100005], oldw[100005];
int cnt, headers[100005], siz[100005];
long long f[100005][3], old[100005][3];
void add(int from,int to,int op)
{
    cnt++;
    que[cnt].from = from;
    que[cnt].to = to;
    que[cnt].op = op;
    que[cnt].next = headers[from];
    headers[from] = cnt;
}
//考虑逐位观察影响
void dfs(int place,int father)
{
    siz[place] = 1;
    f[place][0] = 0;
    f[place][1] = inf;
    f[place][2] = 0;
    for (int i = headers[place]; i; i = que[i].next)
        if(que[i].to!=father)
        {
            dfs(que[i].to, place);
            siz[place] += siz[que[i].to];
            switch(que[i].op)
            {
                case 0:
                {
                    f[place][0] |= w[place] | f[que[i].to][0];
                    f[place][1] &= w[place] | f[que[i].to][1];
                    if (siz[que[i].to] & 1)
                        f[place][2] ^= w[place] | f[que[i].to][2];
                    else
                        f[place][2] ^= (w[place] | f[que[i].to][2]) ^ w[place];
                    break;
                }
                case 1:
                {
                    f[place][0] |= w[place] & f[que[i].to][0];
                    f[place][1] &= w[place] & f[que[i].to][1];
                    f[place][2] ^= w[place] & f[que[i].to][2];
                    break;
                }
                case 2:
                {
                    f[place][0] |= ((~w[place]) & f[que[i].to][0]) | (w[place] & (~f[que[i].to][1]));
                    f[place][1] &= ((~w[place]) & f[que[i].to][1]) | (w[place] & (~f[que[i].to][0]));
                    if (siz[que[i].to] & 1)
                        f[place][2] ^= w[place] ^ f[que[i].to][2];
                    else
                        f[place][2] ^= f[que[i].to][2];
                    break;
                }
            }
        }
    old[place][0] = f[place][0];
    old[place][1] = f[place][1];
    old[place][2] = f[place][2];
    f[place][0] |= w[place];
    f[place][1] &= w[place];
    f[place][2] ^= w[place];
    return;
}
struct node
{
    int id;
    int u;
};
vector<node> ask[100005];
long long ans[100005][3];
int main()
{
    int n, q;
    scanf("%d%d", &n, &q);
    for (int i = 1; i <= n;i++)
    {
        scanf("%lld", &w[i]);
        oldw[i] = w[i];
    }
    for (int i = 1; i < n;i++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, i + 1, b);
        add(i + 1, a, b);
    }
    for (int i = 1; i <= q; i++)
    {
        int u, d;
        scanf("%d%d", &d, &u);
        ask[d].push_back((node){i, u});
    }
    for (int d = 0; d <= 100;d++)
    {
        for (int i = 1; i <= n; i++)
            w[i] = oldw[i] + (long long)i * d;
        dfs(1, 0);
        for(auto i:ask[d])
            for (int j = 0; j <= 2;j++)
                ans[i.id][j] = old[i.u][j];
    }
    for (int i = 1; i <= q;i++)
    {
        for (int j = 0; j <= 2;j++)
            printf("%lld ", ans[i][j]);
        printf("\n");
    }
    return 0;
}

F Finding Points

题意:按照顺时针方向,给定一个凸多边形,求多边形中一点到各边形成的夹角最小值的最大值。

解法:最大值的最小或者最小值的最大,一律二分答案。二分出角度后,对于每条边,可以使用圆心角与圆周角的关系求出对应的轨迹圆。

已知圆周角与对应的弦,求圆。首先圆心一定在弦的中垂线上,将圆周角 α \alpha α 变成圆心角 2 α 2\alpha 2α 2 π − 2 α 2\pi -2\alpha 2π2α。这样圆心、弦中点、线段任意一个顶点构成一直角三角形,使用三角函数即可计算。

剩下的就是求圆的交——枚举每个圆的交点与圆心,看它是否在剩下的圆中。

求两圆交点。首先将圆心纵坐标小的圆放在下部,记为 ( x , y ) (x,y) (x,y),其半径为 r r r。记两圆连心线与 x x x 轴形成的夹角为 α \alpha α,交点弦在下部的圆对应的圆心角为 β \beta β,则交点可以表示为 ( x + r cos ⁡ ( α + β ) , y + r sin ⁡ ( α + β ) ) (x+r\cos (\alpha + \beta),y+r \sin (\alpha + \beta)) (x+rcos(α+β),y+rsin(α+β)) ( x + r cos ⁡ ( α − β ) , y + r sin ⁡ ( α − β ) ) (x+r\cos (\alpha - \beta),y+r \sin (\alpha - \beta)) (x+rcos(αβ),y+rsin(αβ))

整体复杂度 O ( n 3 ) O(n^3) O(n3)

#include <cstdio>
#include <algorithm>
#include <vector>
#include <cmath>
using namespace std;
const long double pi = acos(-1);
const long double eps = 1e-10;
struct node
{
    long double x;
    long double y;
};
long double distance(node a,node b)
{
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
struct vec
{
    long double x;
    long double y;
    long double length()
    {
        return sqrt(x * x + y * y);
    }
};
vec operator -(node a,node b)
{
    return (vec){b.x - a.x, b.y - a.y};
}
node operator +(node a,vec b)
{
    return (node){a.x + b.x, a.y + b.y};
}
vec operator *(long double len,vec a)
{
    return (vec){len * a.x, len * a.y};
}
vec unit(vec a)
{
    return (vec){a.x / a.length(), a.y / a.length()};
}
struct circle
{
    node center;
    long double r;
};
struct polygon
{
    vector<node> vertex;
    int n;
};
vector<circle> cal(polygon now,long double angle)
{
    vector<circle> ans;
    vector<node> temp;
    for (int i = 0; i < now.n;i++)
        temp.push_back(now.vertex[i]);
    temp.push_back(now.vertex[0]);
    for (int i = 0; i + 1 < temp.size(); i++)
    {
        node start = (node){(temp[i].x + temp[i + 1].x) / 2, (temp[i].y + temp[i + 1].y) / 2};
        vec direction = unit((vec){temp[i].y - temp[i + 1].y, temp[i + 1].x - temp[i].x});
        long double dis = distance(temp[i], temp[i + 1]);
        long double len = dis / 2 / tanl(angle);
        long double r = dis / 2 / sinl(angle);
        ans.push_back((circle){start + (-len * direction), r});
    }
    return ans;
}
vector<node> get_intersection(circle a,circle b)
{
    vector<node> ans;
    long double dis = distance(a.center, b.center);
    if(dis>a.r+b.r-eps || -eps+dis<fabs(a.r-b.r))
        return ans;
    if(a.center.y>b.center.y)
        swap(a, b);
    long double alpha = asin((b.center.y - a.center.y)/dis);
    long double beta = (a.r * a.r + dis * dis - b.r * b.r) / (2 * a.r * dis);
    ans.push_back((node){a.center.x + a.r * cosl(alpha + beta), a.center.y + a.r * sinl(alpha + beta)});
    ans.push_back((node){a.center.x + a.r * cosl(alpha - beta), a.center.y + a.r * sinl(alpha - beta)});
    return ans;
}
bool check(vector<circle> &cir)
{
    int n = cir.size();
    for(auto i:cir)
    {
        bool flag = 0;
        for(auto j:cir)
            if (distance(i.center, j.center) > j.r - eps)
            {
                flag = 1;
                break;
            }
        if(!flag)
            return 1;
    }
    for (int i = 0; i < n;i++)
        for (int j = i + 1; j < n;j++)
        {
            long double dis = distance(cir[i].center, cir[j].center);
            if(dis<fabs(cir[i].r-cir[j].r))
                continue;
            vector<node> intersection = get_intersection(cir[i], cir[j]);
            if(intersection.empty())
                return 0;
            for(auto k:intersection)
            {
                bool flag = 0;
                for(auto l:cir)
                    if(distance(k,l.center)>l.r+eps)
                    {
                        flag = 1;
                        break;
                    }
                if(!flag)
                    return 1;
            }
        }
    return 0;
}
int main()
{
    polygon now;
    scanf("%d", &now.n);
    for (int i = 1; i <= now.n;i++)
    {
        double x, y;
        scanf("%lf%lf", &x, &y);
        now.vertex.push_back((node){x, y});
    }
    reverse(now.vertex.begin(), now.vertex.end());
    long double left = 0.0, right = 2 * pi / now.n;
    while(left+eps<right)
    {
        long double mid = (left + right) / 2;
        vector<circle> temp = cal(now, mid);
        if(check(temp))
            left = mid;
        else
            right = mid;
    }
    printf("%.10Lf", left * 180 / pi);
    return 0;
}

G Greater Integer, Better LCM

题意:给定三个数 a , b , c a,b,c a,b,c,求满足 l c m ( a + x , b + y ) = c {\rm lcm}(a+x,b+y)=c lcm(a+x,b+y)=c ( x , y ) (x,y) (x,y) x + y x+y x+y 最小值。满足 c c c 是由不超过 100 100 100 个质数构成,全部次方数不超过 18 18 18。使用 int128。

解法:考虑每一个构成 c c c 的质数 p p p,其次方数为 q q q。由于 l c m \rm lcm lcm 的性质, a + x a+x a+x b + y b+y b+y 中必然要有一个数在这个质因子上次方得达到 q q q 次。此时另一个数可以不达到。

设计 DP 状态: f [ m a s k ] f[mask] f[mask] a + x a+x a+x 满足了多少个质因子的条件(由 mask 的二进制状态表示),得到了一个数 y ≥ a y\geq a ya,记录此时最小的 y − a = x y-a=x ya=x。我们只需要枚举这每一个质数的次方数即可完成这个 DP。最后还需要 1 1 1 少的状态承接 1 1 1 多的状态的答案。

注意到满足 a a a 与满足 b b b 条件是一样的。我们记录对应的 m a s k mask mask 值与得到的乘积 v v v,枚举全部的这样状态,对于 b b b 及其 y y y,使用 v − b v-b vb 得到;而 a a a x x x 的状态,则已经记录在 DP 数组中了,只需要找到 m a s k mask mask 的补状态即可。

总体复杂度 O ( n 2 n ) O(n 2^n) O(n2n) n ≤ 18 n \leq 18 n18

#include <cstdio>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const __int128_t inf = (__int128_t)1e36;
vector<pair<int, __int128_t>> dif;
int n;
void scan(__int128_t &now)
{
    char num[45];
    scanf("%s", num);
    int len = strlen(num);
    for (int i = 0; i < len;i++)
    {
        now *= 10;
        now += num[i] - 48;
    }
    return;
}
void print(__int128_t value)
{
    if(value==0)
    {
        printf("0");
        return;
    }
    vector<int> num;
    while(value)
    {
        num.push_back(value % 10);
        value /= 10;
    }
    for (int i = num.size() - 1; i >= 0;i--)
        printf("%d", num[i]);
    return;
}
__int128_t f[1 << 18], a, b, prime[20];
int times[20];
void dfs(int step,__int128_t value,int mask)
{
    if (step == n + 1)
    {
        dif.push_back(make_pair(mask, value));
        if(value>=a)
            f[mask] = min(f[mask], value - a);
        return;
    }
    for (int i = 0; i <= times[step];i++)
    {
        if(i==times[step])
            mask |= (1 << (step - 1));
        dfs(step + 1, value, mask);
        value *= prime[step];
    }
    return;
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n;i++)
    {
        scan(prime[i]);
        scanf("%d", &times[i]);
    }
    scan(a);
    scan(b);
    for (int i = 0; i < (1 << n);i++)
        f[i] = inf;
    dfs(1, 1, 0);
    for (int i = 0; i < n;i++)
        for (int j = 0; j < (1 << n);j++)
            if ((j & (1 << i)) == 0)
                f[j] = min(f[j], f[j | (1 << i)]);
    __int128_t ans = inf;
    for(auto i:dif)
        if(i.second>=b)
        {
            int fora = (1 << n) - 1 - i.first;
            ans = min(ans, i.second - b + f[fora]);
        }
    print(ans);
    return 0;
}

J Jewels

题意:有 n n n 个宝藏分别位于 ( x i , y i , z i ) (x_i,y_i,z_i) (xi,yi,zi) 处,在第 t t t 时所需挖掘花费为 ( x i + y i + z i + t v i ) 2 (x_i+y_i+z_i+tv_i)^2 (xi+yi+zi+tvi)2,问最小的花费总和。 n ≤ 300 n \leq 300 n300

解法:很容易发现,我们只需要将每一个时刻 t t t 和这一时刻挖掘的宝藏 i i i 所进行匹配即可,匹配的边权就是这一时刻挖掘这一宝藏所需时间,然后使用 KM 算法即可。使用时需要将边权转成负数。

复杂度 O ( n 3 ) O(n^3) O(n3)

#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
const long long inf = 0x3f3f3f3f3f3f3fll;
long long distance(long long x,long long y,long long z)
{
    return x * x + y * y + z * z;
}
long long dis[305][305];
long long ex_left[305], ex_right[305];
//左侧期望匹配权值、右侧期望匹配权值。
int match[305];
long long lack[305];
//右侧需要匹配到左侧的一个点所需的最小权值差。
bool left[305], right[305];
int n;
bool dfs(int now)
{
    left[now] = 1;
    for (int i = 1; i <= n;i++)
    {
        if(right[i])
            continue;
        long long gap = ex_left[now] + ex_right[i] - dis[now][i];
        if(gap==0)
        {
            right[i] = 1;
            if(!match[i] || dfs(match[i]))
            {
                match[i] = now;
                return 1;
            }
        }
        else
            lack[i] = min(lack[i], gap);
    }
    return 0;
}
long long x[305], y[305], z[305], v[305];
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n;i++)
    {
        scanf("%lld%lld%lld%lld", &x[i], &y[i], &z[i], &v[i]);
        for (int j = 1; j <= n; j++)
            dis[j][i] = -distance(x[i], y[i], z[i] + v[i] * (j - 1));
    }
    for (int i = 1; i <= n;i++)
    {
        ex_left[i] = -inf;
        for (int j = 1; j <= n; j++)
            ex_left[i] = max(ex_left[i], dis[i][j]);
    }
    for (int i = 1; i <= n;i++)
    {
        for (int j = 1; j <= n;j++)
            lack[j] = inf;
        while(1)
        {
            for (int j = 1; j <= n;j++)
                left[j] = right[j] = 0;
            if(dfs(i))
                break;
            long long d = inf;
            for (int j = 1; j <= n;j++)
                if(!right[j])
                    d = min(d, lack[j]);
            for (int j = 1; j <= n;j++)
            {
                if(left[j])
                    ex_left[j] -= d;
                if(right[j])
                    ex_right[j] += d;
                else
                    lack[j] -= d;
            }
        }
    }
    long long ans = 0;
    for (int i = 1; i <= n;i++)
        ans -= dis[match[i]][i];
    printf("%lld", ans);
    return 0;
}

K King of Range

题意:给定长度为 n n n 的序列,进行 m m m 次询问,每次问有多少个区间其极差严格大于 k k k n ≤ 1 × 1 0 6 n \leq 1\times 10^6 n1×106 m ≤ 100 m \leq 100 m100

解法:单次询问复杂度可以使用双指针优化到 O ( n ) O(n) O(n)。考虑每次右移最右边界 r r r,然后尽可能移动现有的左边界 l l l 直到无法移动,那么右端点在 r r r 对答案的贡献就是 l l l。这么做的合理性在于,当右端点单增时,最右左边界也是在单增的。

判定时只需要使用 O ( 1 ) O(1) O(1) 的 RMQ 或者单调队列即可实现。RMQ 做法不再赘述。

#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
int a[100005];
int minimum[100005][35], maximum[100005][35];
inline int query_maximum(int left,int right)
{
    int now = left;
    int ans = 0;
    for (int i = 16; i >= 0; i--)
    {
        if (now + (1 << i) <= right + 1)
        {
            ans = max(ans, maximum[now][i]);
            now += (1 << i);
        }
    }
    return ans;
}
inline int query_minimum(int left,int right)
{
    int now = left;
    int ans = 999999999;
    for (int i = 16; i >= 0; i--)
    {
        if (now + (1 << i) <= right + 1)
        {
            ans = min(ans, minimum[now][i]);
            now += (1 << i);
        }
    }
    return ans;
}
int main()
{
    int n, m, k;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++)
        maximum[i][0] = minimum[i][0] = a[i];
    for (int j = 1; (1 << j) <= n; j++)
        for (int i = 1; i + (1 << j) - 1 <= n; i++)
        {
            minimum[i][j] = min(minimum[i][j - 1], minimum[i + (1 << (j-1))][j - 1]);
            maximum[i][j] = max(maximum[i][j - 1], maximum[i + (1 << (j-1))][j - 1]);
        }
    while(m--)
    {
        long long ans = 0;
        scanf("%d", &k);
        int left = 0, right = 0;
        for (right = 1; right <= n;right++)
        {
            while (left <= right && right <= n)
            {
                if (left && query_maximum(left, right) - query_minimum(left, right) <= k)
                {
                    left--;
                    break;
                }
                left++;
            }
            if (left && right <= n && query_maximum(left, right) - query_minimum(left, right) > k)
                ans += left;
        }
        printf("%lld\n", ans);
    }
    return 0;
}

单调队列做法为:维护两个单调队列——一个单增一个单减,每次配合所在的 [ l , r ] [l,r] [l,r] 进行插入与弹出。然后直接统计两个队首元素的差值就是极差。二者的复杂度均为 O ( n m ) O(nm) O(nm),单次询问摊还复杂度均为 O ( n ) O(n) O(n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值