2020牛客暑假多校第二场补题

比赛链接:link


   

A kmp + Hash

   题意是说,定义了两个字符串间的函数 f ( s , t ) f(s, t) f(s,t) 表示字符串 s s s 的前缀和字符串 t t t 后缀能相等的最大长度,而总共有 n n n 个串,求 ∑ i = 1 n ∑ j = 1 n f ( s i , s j ) \sum_{i = 1}^{n} \sum_{j=1}^{n} f(s_i, s_j) i=1nj=1nf(si,sj)。其中 1 ≤ n ≤ 1 e 5 1 ≤ n ≤ 1e5 1n1e5, ∑ ∣ s i ∣ ≤ 1 e 6 \sum|s_i| ≤ 1e6 si1e6
   比赛中这题过的人不是很多,感觉也没有很巧的解法。暴力的话,我们可以先将每个字符串的后缀处理出来,由于 ∑ ∣ s i ∣ ≤ 1 e 6 \sum|s_i| ≤ 1e6 si1e6,那么最多有 1 e 6 1e6 1e6 个后缀,然后再遍历前缀来和存下来的后缀匹配。但是这样的话一定是会重复的,比如只有一个串 a b a aba aba,我存的后缀有 a a a, b a ba ba, a b a aba aba, 那么我遍历前缀的时候 a a a a b a aba aba 都可以匹配到,但是我们只需要长度最大的那个,这该如何去重呢?
   我们可以再举一个例子来看一看,比如 a b a b a ababa ababa 与自身匹配,进行遍历前缀时:
   ① a a a 匹配到,所以 a n s [ 1 ] + + ans[1]++ ans[1]++(记录个数);
   ② a b ab ab匹配不到;
   ③ a b a aba aba 匹配得到,所以我们现在知道 a a a 会重复,所以 a n s [ 1 ] − − , a n s [ 3 ] + + ans[1]--, ans[3]++ ans[1],ans[3]++
   ④ a b a b abab abab 匹配不到;
   ⑤ a b a b a ababa ababa 匹配到,所以我们现在知道 a b a aba aba 重复,所以 a n s [ 3 ] − − , a n s [ 5 ] + + ans[3]--, ans[5]++ ans[3],ans[5]++
   所以我们每匹配到一个前缀,就要减去其能相等的最大长度的前后缀的计数(不包括自身),而这正好就是 kmp 算法里的 next 数组(或者叫 fail 失败链接)
   而存后缀的话,翻了翻 AC 代码,大部分人都是通过函数 ∑ i = 0 l e n − 1 s [ i ] ∗ 13 1 i \sum_{i = 0}^{len-1} s[i] * 131^i i=0len1s[i]131i 将字符串转为 unsigned long long, 然后用 map 进行映射,所以我也采取了这种哈希方式,这样的映射稍微长一点的字符串肯定会自然溢出,但是相等的字符串一定能映射成相同的数值
   需要注意的是,用普通的 map 花了 2.4 s 2.4s 2.4s, 而用 unordered_map 花了 0.9 s 0.9s 0.9s,所以如果卡 map 的常数,一定要用
unordered_map

   

#include <bits/stdc++.h>
 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 1e5 + 10;
const int maxm = 1e6 + 10;
const ll mod = 998244353;
 
int nxt[maxm], n;
ll ans, num[maxm];
string p[maxn];
unordered_map<ull, int> mp;
 
void calnext(int k)                 //nxt[i] 表示字符串下标[0, i-1](即长度为i)的最长前后缀相等长度(不包括自身)
{
    nxt[0] = -1;
    int i = 1, j;
    while(p[k][i-1])
    {
        j = nxt[i-1];
        while(j != -1 && p[k][j] != p[k][i-1])
            j = nxt[j];
        nxt[i] = j + 1;
        i++;
    }
}
 
int main()
{
    cin>>n;
    for(int i = 1; i <= n; i++)
        cin>>p[i];
 
    for(int i = 1; i <= n; i++)
    {
        ull cur = 0, base = 1;
        for(int j = p[i].size() - 1; j >= 0; j--)        //计算哈希值
        {
            cur = cur + base * p[i][j];
            base *= 131;
            mp[cur]++;                     //用map给后缀计数
        }
    }
 
    for(int i = 1; i <= n; i++)
    {
        calnext(i);
        ll cur = 0;
        for(int j = 0; p[i][j]; j++)
        {
            cur = cur * 131 + p[i][j];         //计算哈希值
            num[j+1] = mp[cur];
            if(nxt[j+1]) num[nxt[j+1]] -= num[j+1];         //去重
        }
        for(int j = 1; p[i][j-1]; j++)                 //计算题目所需要的那个值
            ans += num[j] * j % mod * j % mod, ans %= mod;
    }
 
    cout<<ans<<endl;
}

    然后还有很多大佬用了自己写的 Hash_map…还有 AC 自动机的做法, 待补。
   

B 几何

    题目大意是给了 n   ( n ≤ 2000 ) n \ (n ≤ 2000) n (n2000) 个二维坐标点,问最多多少个点可以共圆 (这个圆必须经过原点)。
   看这个数据量,应该是平方的,可以枚举。由于三点不共线确定一个圆,而圆必过原点,只要枚举剩下两个顶点就好了。当确定了原点和点 i i i 之后,如果原点,点 i i i,点 j 1 j_1 j1 确定的圆心与原点,点 i i i,点 j 2 j_2 j2 确定的圆心相等,那么 i i i, j 1 j_1 j1, j 2 j_2 j2 就可以共圆。
   所以我们需要记录圆心,通过数学推导, ( 0 , 0 ) , ( x 1 , y 1 ) , ( x 2 , y 2 ) (0, 0), (x_1, y_1), (x_2, y_2) (0,0),(x1,y1),(x2,y2) 若是不共线,即 k = x 1 y 2 − x 2 y 1 ≠ 0 k = x_1 y_2 - x_2 y_1 ≠ 0 k=x1y2x2y1=0
X = y 2 ( x 1 2 + y 1 2 ) − y 1 ( x 2 2 + y 2 2 ) 2 k X = \frac{y_2 (x_1^2 + y_1^2) - y_1 (x_2^2 + y_2^2)}{2k} X=2ky2(x12+y12)y1(x22+y22) Y = x 1 ( x 2 2 + y 2 2 ) − x 2 ( x 1 2 + y 1 2 ) 2 k Y = \frac{x_1 (x_2^2 + y_2^2) - x_2 (x_1^2 + y_1^2)}{2k} Y=2kx1(x22+y22)x2(x12+y12)
   这个算出来是浮点数,我比赛的时候觉得用 map 去存肯定有精度误差,所以就不敢(但是居然这样可以过…),我是记录完再排序,若是两个相邻点的误差在 eps 范围内,就认为相等。其实更精确的方法是用分数形来记录,但是这样会超时…
   由于遍历记录完还要排序,那么总复杂度为 O ( n l o g n ) O(nlogn) O(nlogn), wa 了好多发的原因是没有开 long long,哎…
   

#include <bits/stdc++.h>
 
using namespace std;
typedef long long ll;
const double eps = 1e-11;
 
struct node
{
    double x, y;
    bool operator<(const node& m1)
    {
        if(x != m1.x)
            return x < m1.x;
        return y < m1.y;
    }
}pro[2005];
 
int n, ans, cnt, Point[2005][2];
 
void cal(int x1, int y1, int x2, int y2)       //计算圆心
{
    int k = 2 * (x1 * y2 - x2 * y1);
    if(k == 0)
        return;
    int k1 = x1 * x1 + y1 * y1, k2 = x2 * x2 + y2 * y2;
    pro[cnt].x = 1.0 * (1LL * y2 * k1 - 1LL * y1 * k2) / k;
    pro[cnt++].y = 1.0 * (1LL * x1 * k2 - 1LL * x2 * k1) / k;
}
 
int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%d %d", &Point[i][0], &Point[i][1]);
    if(n == 1)                  //若是只有一个点
    {
        printf("%d\n", 1);
        return 0;
    }
    for(int i = 1; i <= n; i++)       //确定原点和点i,遍历其他点
    {
        cnt = 0;
        for(int j = i + 1; j <= n; j++)
            cal(Point[i][0], Point[i][1], Point[j][0], Point[j][1]);
        sort(pro, pro + cnt);
        if(cnt == 0)                //如果所有点都与原点和点i确定的直线共线
        {
            ans = max(1, ans);
            continue;
        }
        int tmp = 1;
        for(int m = 1; m < cnt; m++)
        {
            if(abs(pro[m].x - pro[m-1].x) < eps && abs(pro[m].y - pro[m-1].y) < eps)   //统计相同圆心
                tmp++;
            else
            {
                ans = max(ans, tmp + 1);
                tmp = 1;
            }
        }
        ans = max(tmp + 1, ans);
    }
 
    printf("%d\n", ans);
}

   

C dfs

   题目是说给定一棵树,求最少的链,保证每个点都至少被一条链覆盖。
   由于树可能很大,那么树的形状那么多,所以觉得可能和具体的树的形状无关。考虑到叶子结点必须被覆盖,且叶子结点至少占了一条链的一端,那么至少需要 c e i l ( n u m ( l e a f ) 2 ) ceil(\frac{num(leaf)}{2}) ceil(2num(leaf)) 条链。试了很多树,发现这么多链是可以覆盖所有树的。
   但是并非任意的叶子结点相连形成链都可以,比如下图:
在这里插入图片描述   
   如果链 1-2, 3-4 的话,结点 0 就不会覆盖到,必须要 1 与 3 连接, 2 与 4 连接,所以我们可以 dfs 记录所有的叶子结点,然后让前半部分的叶子结点与后半部分的叶子结点相连,如 1 与 3, 2 与 4。
   

#include <bits/stdc++.h>

using namespace std;
const int maxn = 2e5 + 10;

vector<int> G[maxn];
int leaf[maxn], cnt, n;

void dfs(int x, int fa)
{
    if(G[x].size() == 1)
    {
        leaf[++cnt] = x;
    }

    for(unsigned int i = 0; i < G[x].size(); i++)
    {
        if(G[x][i] == fa) continue;
        dfs(G[x][i], x);
    }
}

int main()
{
    scanf("%d", &n);
    if(n == 1)
    {
        printf("%d\n%d %d", 1, 1, 1);
        return 0;
    }
    for(int i = 1; i < n; i++)
    {
        int x, y;
        scanf("%d %d", &x, &y);
        G[x].push_back(y);
        G[y].push_back(x);
    }

    dfs(1, 0);
    printf("%d\n", (cnt + 1) / 2);
    for(int i = 1; i <= (cnt + 1) / 2; i++)
        printf("%d %d\n", leaf[i], leaf[i+cnt/2]);
}

   

D 签到题

   只需要把时间换成秒一剪就好啦,用 scanf 读就很舒服,可以处理掉 :。

#include <bits/stdc++.h>

using namespace std;

int main()
{
    int x, y, z, ans1, ans2;
    scanf("%d:%d:%d", &x, &y, &z);
    ans1 = x * 3600 + y * 60 + z;

    scanf("%d:%d:%d", &x, &y, &z);
    ans2 = x * 3600 + y * 60 + z;
    printf("%d\n", abs(ans1 - ans2));
}

   

F 单调区间 + gcd筛

   题意是说给定一个 n × m n×m n×m 的矩阵 A A A,其中 A i j = l c m ( i , j ) A_{ij} = lcm(i, j) Aij=lcm(i,j),对于大矩阵每一个 k × k k × k k×k 的子矩阵,都有一个最大值,我们求这些最大值的和。
   感觉矩阵里的每个元素还是要算出来的, 要是直接算的话,复杂度为 O ( n m l o g n ) O(nmlogn) O(nmlogn), 可以用对称相等来稍微优化一下。标程给了一种筛法,可以去掉那个log, 感觉有点像埃筛:

for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= m; j ++)
			if (!A[i][j])
				for (int k = 1; k * i <= n && k * j <= m; k ++)
					A[k * i][k * j] = k;
	for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= m; j ++)
			A[i][j] = i * j / A[i][j];

   带个 log 也能过,接下来就是找最大值了。对于一维区间,遍历长度为 k k k 的最大值 可以用经典的单调队列来做,而这个二维矩阵我们只需要对两维都来一遍就好了,这里对单调队列有一个比较详细的说明,就不赘述(link)
   

#include <bits/stdc++.h>
 
using namespace std;
typedef long long ll;
const ll mod = 998244353;
 
int d[5002][5002], n, m, k, du[5002];
 
int gcd(int x, int y)
{
    if(x % y) return gcd(y, x % y);
    return y;
}
 
int main()
{
    scanf("%d %d %d", &n, &m, &k);
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= m; j++)
        {
            if(i <= min(m, n) && i > j) d[i][j] = d[j][i];   //对称位置值相同
            else d[i][j] = i / gcd(i, j) * j;
        }
    }
 
    for(int i = 1; i <= n; i++)           //用单调队列先求列长度为 k 时的最大值
    {
        int l = 0, r = 1;
        du[0] = 1;
        if(k == 1) continue;
        for (int j = 2; j <= m; j++)
        {
            if (j - du[l] >= k && (l < r))
                l++;
            while (r > l && d[i][du[r - 1]] <= d[i][j])
                r--;
            du[r++] = j;
            if (j >= k)
                d[i][j-k+1] = d[i][du[l]];
        }
    }
 
    for(int j = 1; j <= m - k + 1; j++)     //再用单调队列先求行长度为 k 时的最大值
    {
        int l = 0, r = 1;
        du[0] = 1;
        if(k == 1) continue;
        for (int i = 2; i <= n; i++)
        {
            if (i - du[l] >= k && (l < r))
                l++;
            while (r > l && d[du[r - 1]][j] <= d[i][j])
                r--;
            du[r++] = i;
            if (i >= k)
                d[i-k+1][j] = d[du[l]][j];
        }
    }
 
    ll ans = 0;
    for(int i = 1; i <= n - k + 1; i++)
        for(int j = 1; j <= m - k + 1; j++)
            ans += d[i][j];
    printf("%lld\n", ans);
}

   

G bitset神奇用法

   题目大意是给定长度为 n n n 的序列 A 和长度为 m m m 的序列 B,其中 n ≥ m n ≥ m nm, A中可以截取长度为 m m m 的连续区间 S S S, 问满足对于任意 1 ≤ i ≤ m 1 ≤i ≤ m 1im, 都有 S i ≥ B i S_i ≥ B_i SiBi 的区间个数。其中 m ≤ 4 e 4 , n ≤ 1.5 e 5 m ≤ 4e4,n ≤ 1.5e5 m4e4n1.5e5
   看了好久才看懂标程…首先为了之后的状态转移,对于每一个 A i A_i Ai,都有一个 bitset I [ i ] I[i] I[i],若 I [ i ] [ j ] = 1 I[i][j] = 1 I[i][j]=1, 表示 A i ≥ B j A_i ≥ B_j AiBj, 反之为 0 表示 A i < B j A_i < B_j AiBj。若是单纯的暴力匹配得 bitset 的值,我们的空间复杂度和时间复杂度都是 O ( n m 64 ) O(\frac{nm}{64}) O(64nm), 所以我们可以考虑先排序,用双指针的方式进行遍历。因为若 A i 1 ≤ A i 2 A_{i_1} ≤ A_{i_2} Ai1Ai2,那么 I [ i 1 ] I[i_1] I[i1] 为 1 的地方, I [ i 2 ] I[i_2] I[i2] 也一定为1, 所以排序之后,后一个数的 bitset 可以在前一个数 bitset 的基础上进行修改。
   而其实我们也不需要 n n n 个 bitset, 因为序列 B 长度为 m m m, 所以最多有 m + 1 m + 1 m+1 个不同的 bitset, 这样空间复杂度可以降到 O ( m 2 64 ) O(\frac{m^2}{64}) O(64m2)
   接下来比较重要的就是这个转移方法了,我们用一个长度为 m + 1 m + 1 m+1 的 bitset,来记录状态,若第 i i i 位为 1,表示当前可以匹配到 序列B 的前 i i i 位,否则表示没有匹配到。这个还是需要例子来说明,若 A = [ 1 , 2 , 2 , 3 , 5 ] , B = [ 1 , 2 , 3 ] A = [1, 2, 2, 3, 5], B = [1, 2, 3] A=[1,2,2,3,5],B=[1,2,3], bitset 初始为 [ 1 , 0 , 0 , 0 ] [1, 0, 0, 0] [1,0,0,0](第 0 位为 1 表示可以匹配到的区间长度为0)
   ①当前匹配到区间长度为0,我们尝试去扩展区间,由于 A [ 1 ] ≥ B [ 1 ] A[1] ≥ B[1] A[1]B[1],bitset 变成 [ 1 , 1 , 0 , 0 ] [1, 1, 0, 0] [1,1,0,0]
   ②当前匹配到区间长度为0或1,我们尝试去扩展区间,由于 A [ 2 ] ≥ B [ 1 ] , A [ 2 ] ≥ B [ 2 ] A[2] ≥ B[1],A[2] ≥ B[2] A[2]B[1]A[2]B[2], bitset 变成 [ 1 , 1 , 1 , 0 ] [1, 1, 1, 0] [1,1,1,0]
   ③当前匹配到区间长度为0或1或2,我们尝试去扩展区间,由于 A [ 3 ] ≥ B [ 1 ] , A [ 3 ] ≥ B [ 2 ] A[3] ≥ B[1],A[3] ≥ B[2] A[3]B[1]A[3]B[2],但 A [ 3 ] < B [ 3 ] A[3] < B[3] A[3]B[3], bitset 变成 [ 1 , 1 , 1 , 0 ] [1, 1, 1, 0] [1,1,1,0]
   ④当前匹配到区间长度为0或1或2,我们尝试去扩展区间,由于 A [ 4 ] ≥ B [ 1 ] , A [ 4 ] ≥ B [ 2 ] , A [ 4 ] ≥ B [ 3 ] A[4] ≥ B[1],A[4] ≥ B[2],A[4] ≥ B[3] A[4]B[1]A[4]B[2]A[4]B[3],bitset 变成 [ 1 , 1 , 1 , 1 ] [1, 1, 1, 1] [1,1,1,1], 答案+1;
   ④当前匹配到区间长度为0或1或2,我们尝试去扩展区间,由于 A [ 5 ] ≥ B [ 1 ] , A [ 5 ] ≥ B [ 2 ] , A [ 5 ] ≥ B [ 3 ] A[5] ≥ B[1],A[5] ≥ B[2],A[5] ≥ B[3] A[5]B[1]A[5]B[2]A[5]B[3],bitset 变成 [ 1 , 1 , 1 , 1 ] [1, 1, 1, 1] [1,1,1,1], 答案+1;
    扩展区间操作相当于将当前 bitset 向左移一位,然后与 I [ i ] I[i] I[i] 进行与操作: c u r = ( c u r < < 1 ) & I [ i ] cur = (cur<<1) \& I[i] cur=(cur<<1)&I[i]

   

#include <bits/stdc++.h>
 
using namespace std;
typedef long long ll;
const int maxn = 1.5e5 + 10;
const int maxm = 4e4 + 10;
 
bitset<40010> I[40010];
int n, m, a[maxn], b[maxm], k1[maxn], k2[maxm], mark[maxn], tol, ans;
 
int main()
{
    scanf("%d %d", &n, &m);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]);
        k1[i] = i;
    }
    for(int i = 1; i <= m; i++)
    {
        scanf("%d", &b[i]);
        k2[i] = i;
    }
 
    sort(k1 + 1, k1 + 1 + n, [](int x, int y){return a[x] < a[y];});    //k1[i] 表示 a 数组第i小的数的下标  
    sort(k2 + 1, k2 + 1 + m, [](int x, int y){return b[x] < b[y];});
 
    bitset<40010> tmp;
    int p = 1, flag;
    for(int i = 1; i <= n; i++)
    {
        flag = 0;
        while(p <= m && a[k1[i]] >= b[k2[p]])      //双指针标记bitset
        {
            tmp.set(k2[p]);
            p++;
            flag = 1;
        }
        if(flag) I[++tol] = tmp;
        mark[k1[i]] = tol;
    }
 
    bitset<40010> cur;
    for(int i = 1; i <= n; i++)
    {
        cur.set(0);                  //第0位始终为1,因为总可以从区间长度为0开始扩展
        cur = (cur<<1) & I[mark[i]];
        if(cur[m] == 1)
            ans++;
    }
 
    printf("%d\n", ans);
}

   然后翻别的大佬的 AC 代码,看到一个只用两个 bitset 就过了的…太神仙了,理解了好久,不太能写出来…大致思路是设置一个长度为 n n n 的 bitset, 一开始全部初始化为 1,第 i i i 位为1表示从当前开始长度为 m m m 的区间满足要求,然后通过从大到小遍历来去掉不可能的位置。这内存压的太nb了。
   

#include <bits/stdc++.h>
 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<double, double> P;
const int maxn = 1.5e5 + 10;
const int maxm = 4e4 + 10;
const int INF = 0x3f3f3f3f;
const double eps = 1e-11;
const ll mod = 998244353;
 
bitset<maxn> tmp, cur;
int n, m, a[maxn], b[maxm], k1[maxn], k2[maxm];
 
int main()
{
    scanf("%d %d", &n, &m);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]);
        k1[i] = i;
        cur.set(i);      //全部初始化成1
    }
    for(int i = 1; i <= m; i++)
    {
        scanf("%d", &b[i]);
        k2[i] = i;
    }
 
    sort(k1 + 1, k1 + 1 + n, [](int x, int y){return a[x] > a[y];});
    sort(k2 + 1, k2 + 1 + m, [](int x, int y){return b[x] > b[y];});
 
    int p = 1;
    for(int i = 1; i <= m; i++)        //从大到小遍历
    {
        while(p <= n && a[k1[p]] >= b[k2[i]])
            tmp.set(k1[p++]);
        cur &= tmp >> (k2[i] - 1);       //不满足条件的位置会被置为0
    }
 
    printf("%d\n", cur.count());
}

   

H 权值线段树(动态开点/离散化)

   大致题意是说给定一个允许有重复整数元素的集合,第一种操作是增加一个整数,第二种操作是删除一个整数,第三种操作是给定一个整数,判断是否能从集合内再找两个整数组成一个三角形。
   第一种操作和第二种操作直接用 STL 的 multiset 就可以做到,但是第三种操作就不好维护了。
   若是判断的整数 x x x是三角形的最大边,那么只需要在小于等于 x x x 的集合元素中挑选两个最大的相加判断是否大于 x x x;若是判断的整数是中间边,只需要挑选小于等于 x x x 的最大元素和大于等于 x x x 的最小元素就可以;若是最小边的话,在大于等于 x x x 的元素里挑选,一定是挑选两条差值最小的边。所以最关键的还是动态的记录相邻边的差值
   
   赛后看了别人的解法,也是类似的,第一种第二种操作用 map 来记录个数即可,若记第三种操作挑选的元素是 a , b   ( b ≥ a ) a, b \ (b ≥ a) a,b (ba)那么能组成三角形等价于 b − a < x < b + a b - a < x < b + a ba<x<b+a,若是我们记 b b b 的前驱结点为 b ′ b' b (小于等于 b b b 的最大整数,可以相等),那么我们有 b − b ′ ≤ b − a < x < b + a ≤ b + b ′ b - b' ≤ b - a < x < b + a ≤ b + b' bbba<x<b+ab+b,那么若存在 a a a 可以, b ′ b' b 一定可以。
   我们记 k = l o w e r _ b o u n d ( x / 2 + 1 ) k = lower \_ bound(x / 2 + 1) k=lower_bound(x/2+1),那么 b m i n ≥ k b_{min} ≥ k bmink, 若是 k + k ′ ≤ x k + k' ≤ x k+kx,那么 b m i n = k . n e x t b_{min} = k.next bmin=k.next,否则 b m i n = k b_{min} = k bmin=k (这里可以仔细想一想),而我们只需要判断在 大于等于 b m i n b_{min} bmin 的元素中,有没有和前驱元素差值小于 x x x 的,这个就由权值线段树来维护。
   由于区间可以到 1 e 9 1e9 1e9,所以采取了动态开点的方式(也是第一次学了这种操作),大部分与普通线段树差不多,理解理解代码就好啦。若是添加一个元素集合里没有,那么对后面,自身元素有影响,若是集合里只有一个,那么对自身有影响;若是删除一个元素后集合里只有一个,那么对自身有影响,若是集合里就没有了,那么对自身和后面元素有影响。
   

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<double, double> P;
const int maxn = 2e5 + 10;
const int MAX = 1e9;
const int INF = 0x3f3f3f3f;
const double eps = 1e-11;
const ll mod = 998244353;

struct node
{
    int val, l, r;
}tree[maxn<<2];                          //这棵线段树记录某个点和其前驱结点(小于等于它本身的最大结点)的差值

int cnt, q, root;
map<int, int> mp;

void update(int &id, int l, int r, int pos, int val)      //单点修改,将位置pos的值更新为val
{
    if(!id)                               //如果该结点还没有扩展开
        id = ++cnt, tree[id].val = val;
    if(l == r)
    {
        tree[id].val = val;
        return;
    }

    int ans = 2e9, mid = (l + r) / 2;
    if(pos <= mid)  update(tree[id].l, l, mid, pos, val);
    else  update(tree[id].r, mid + 1, r, pos, val);

    if(tree[id].l)  ans = min(ans, tree[tree[id].l].val);      //if判断该结点是否被扩展开
    if(tree[id].r)  ans = min(ans, tree[tree[id].r].val);
    tree[id].val = ans;
}

int query_min(int id, int l, int r, int x, int y)                //查询[x, y]区间的最小值
{
    if(!id)  return 2e9;                                 //如果该区间没有结点记录,返回INF
    if(x <= l && y >= r)  return tree[id].val;

    int ans = 2e9, mid = (l + r) / 2;
    if(x <= mid)  ans = min(ans, query_min(tree[id].l, l, mid, x, y));
    if(y > mid)  ans = min(ans, query_min(tree[id].r, mid + 1, r, x, y));
    return ans;
}

void add(int x)
{
    mp[x]++;
    if(mp[x] == 1)                       //如果该结点的值是第一次加入
    {
        auto it = mp.lower_bound(x);
        ++it;
        if(it != mp.end() && it->second == 1)         //如果与后驱结点的差值可更新
            update(root, 1, MAX, it->first, it->first - x);
        it--;
        if(it == mp.begin()) update(root, 1, MAX, x, 2e9);         //得到该结点与前驱结点的差值
        else update(root, 1, MAX, x, x - (--it)->first);
    }
    else if(mp[x] == 2)
        update(root, 1, MAX, x, 0);
}

void del(int x)
{
    if(--mp[x] > 1) return;

    auto it = mp.lower_bound(x);
    int l = -MAX;
    if(it != mp.begin())
    {
        l = (--it)->first;
        it++;
    }
    if(mp[x] == 0)
    {
        update(root, 1, MAX, x, 2e9);                  //更新本身
        it++;
        if(it != mp.end() && it->second == 1)               //更新后驱结点
            update(root, 1, MAX, it->first, it->first - l);
        mp.erase(x);                  //这一步很关键,等于0就要从map中删除
    }
    else
        update(root, 1, MAX, x, x - l);
}

bool check(int x)
{
    auto it = mp.lower_bound(x / 2 + 1), ip = it;
    if(it == mp.end())  return false;

    if(it -> second > 1)  return true;
    else if(it == mp.begin() || (--ip) -> first + it->first <= x)
        it++;
    if(it == mp.end())  return false;
    return query_min(1, 1, MAX, it -> first, MAX) < x;
}

int main()
{
    scanf("%d", &q);
    for(int i = 1; i <= q; i++)
    {
        int x, y;
        scanf("%d %d", &x, &y);
        if(x == 1)  add(y);
        else if(x == 2)  del(y);
        else
        {
            if(check(y)) puts("Yes");
            else puts("No");
        }
    }
}

   除了动态开点还有离散化的做法,待补。
   

J 群论

   题意是说一开始给你一个排列 { 1 , 2 , 3... n } \{1, 2, 3...n\} {1,2,3...n},经过 k k k 次置换,变成 { a 1 , a 2 . . . . a n } \{a_1, a_2....a_n\} {a1,a2....an},问若是将原来的排列只置换一次,会变成什么?( 1 ≤ n ≤ 1 e 5 1 ≤ n ≤ 1e5 1n1e5, k k k 为质数)
   
   首先什么是排列的置换呢?根据抽象代数里的定义,一个集合的排列置换是自身对自身的一个双射。 这可以理解成将一个排列的元素打乱顺序,得到一个新的排列。比如排列 { 1 , 2 , 3 , 4 , 5 } \{1, 2, 3, 4, 5\} {1,2,3,4,5} 可以经过 k k k 次置换变成了 { 5 , 3 , 4 , 1 , 2 } \{5, 3,4,1,2\} {5,3,4,1,2}
   而排列是可以分解成若干 cycle 的。比如上面的排列,原来的第 1 位 变成了原来的第 5 位, 第 5 位变成了原来的第 2 位,第 2 位变成了原来的第 3 位,第 3 位变成了原来的第 4 位,第 4 位变成了原来的第 1 位,这样就可以写作一个 cycle : ( 1 , 5 , 2 , 3 , 4 ) (1, 5, 2, 3, 4) (1,5,2,3,4)。再比如 k k k 次置换变成了 { 5 , 3 , 4 , 2 , 1 } \{5, 3,4,2,1\} {5,3,4,2,1}, 可以写作 2 个 cycle: ( 1 , 5 ) ( 2 , 3 , 4 ) (1,5)(2,3,4) (1,5)(2,3,4)
   我们可以发现,若经过 k k k 次置换变成了 { 5 , 3 , 4 , 1 , 2 } \{5, 3,4,1,2\} {5,3,4,1,2},那么经过 5 k 5k 5k 次置换可以变回 { 1 , 2 , 3 , 4 , 5 } \{1,2,3,4,5\} {1,2,3,4,5}。其实 k k k 次置换可以看成一个双射函数,而 t k tk tk 次置换就是一个复合函数,接下来就推一推。
在这里插入图片描述   
    k k k次变换对应的 cycle 即为上图,以第 1 1 1 位为例,一开始是 1 1 1:
   ①经过 k k k 次变换后变成了原来的第 5 5 5 位,所以 k k k 次变换后为 5 5 5
   ②经过 2 k 2k 2k 次变换后变成了 k k k次变换后的 第 5 5 5 位,即没有变换时的第 2 2 2 位,所以 2 k 2k 2k 次变换后为 2 2 2
   ③经过 3 k 3k 3k 次变换后变成了 2 k 2k 2k次变换后的 第 5 5 5 位,即 k k k次变换后的 第 2 2 2 位,没有变换时的第 3 3 3 位,所以 3 k 3k 3k 次变换后为 3 3 3
   ④经过 4 k 4k 4k 次变换后变成了 3 k 3k 3k次变换后的 第 5 5 5 位,即 2 k 2k 2k次变换后的 第 2 2 2 位, k k k次变换后的 第 3 3 3 位,没有变换时的第 4 4 4 位,所以 4 k 4k 4k 次变换后为 4 4 4
   ⑤经过 5 k 5k 5k 次变换后又回到了 1 1 1
   若记上述的 cycle 的变换关系为 b [ ] = { 1 , 5 , 2 , 3 , 4 } b[] = \{1, 5, 2, 3, 4\} b[]={1,5,2,3,4},那么 经过 t k tk tk 次变换后 a [ 1 ] = b [ ( t + 1 ) %   5 ] a[1] = b[(t +1)\% \ 5] a[1]=b[(t+1)% 5]推广一下可以得到 经过 t k tk tk 次变换后: a [ i ] = b [ ( t + i ) %   l e n ( c y c l e ) ] a[i] = b[(t + i)\% \ len(cycle)] a[i]=b[(t+i)% len(cycle)]
   我们在这里可以知道,一个 cycle 的元素要返回原来的位置,要经过 l e n ( c y c l e ) ∗ k len(cycle) * k len(cycle)k 次变换;若得到所有 cycle 的 lcm, 那么整个排列要返回自身就是 l c m ∗ k lcm * k lcmk 次变换。

   当我们知道了一个 cycle t k tk tk 次变化后对应到什么,那么对于一个排列可以分解为若干 cycle, 分开处理即可。我们最终要求的是置换 1 1 1 次的结果,即对于每一个 cycle,长度为 l i l_i li, 都进行 t i k t_ik tik 次变换,其中 t i k ≡ 1   ( m o d   l i ) t_ik ≡ 1 \ (mod \ l_i) tik1 (mod li), k k k 对于 l i l_i li 的逆元,若是有一个同余方程无解,则说明解不存在,但是由于 k k k 为质数,所以 t i t_i ti 肯定有解,可以通过枚举或者扩展欧几里得的方法得到 t i t_i ti
   由于对于每一个 cycle 都可以线性时间得到逆元和进行置换,所以总复杂度为 O ( n ) O(n) O(n)
   

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int maxn = 1e5 + 10;
const int INF = 0x3f3f3f3f;

int ans[maxn], a[maxn], visit[maxn];
int n, k;
int main()
{
    scanf("%d %d", &n, &k);
    for(int i = 1; i <= n; i++)
        scanf("%d", &a[i]);

    for(int i = 1; i <= n; i++)
    {
        if(visit[i]) continue;          //通过 dfs 得到每个 cycle,记录变换关系

        vector<int> v;
        int j = i, inv;
        while(!visit[j])
        {
            v.push_back(j);
            visit[j] = 1;
            j = a[j];
        }
        for(inv = 1; inv < v.size(); inv++)                  //找到相应的逆元
            if(1LL * inv * k % v.size() == 1)  break;
        for(int h = 0; h < v.size(); h++)                    //根据推得的结果进行变换
            ans[v[h]] = v[(h+inv)%v.size()];
    }

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值