首页 > 编程语言 >《随机算法在信息学竞赛中的应用》做题记录

《随机算法在信息学竞赛中的应用》做题记录

时间:2023-01-07 08:22:05浏览次数:66  
标签:信息学 竞赛 frac int 复杂度 ge 算法 向量

目录

《随机算法在信息学竞赛中的应用》做题记录

随机艺术 题单

MSTONES

Problem

\(T\) 组数据,给出 \(n\) 个点的坐标,求一条直线最多能覆盖多少个点。

\(T \le 30\),\(n\le 10^4\)。

Solution

朴素算法:枚举两个点,数有多少个点在两点确定的直线上,取最大值。时间复杂度 \(O(Tn^3)\)。

向朴素算法中加入随机,改造为 Monte Carlo 算法。

由于存在不超过 \(7\) 条直线覆盖完了所有点,所以:

\(ans \ge \lfloor \frac{n}{7} \rfloor\)

从而有:

从 \(n\) 个点中随机选择两个点,过两点的直线与覆盖了最多点的直线重合的概率最小为 \(\frac{1}{49}\)。

即一个点在覆盖了最多点的直线上的概率至少为 \(\frac{1}{7}\),两个点乘法原理即可。

所以如果随 \(k\) 次,正确率为:\(1 - (1 - \frac{1}{49})^{k}\)。

取 \(k = 1000\),正确率约为 \(1.1 \times 10^{-9}\),运行次数为 \(1000nT\)。可以接受。

#include<bits/stdc++.h>
using namespace std;
template<typename T> void chkmx(T &a, const T &b)
{
    (a < b) && (a = b);
}
const int N = 1e4 + 5;
int T, n, ans;
struct node
{
    int x, y;
}a[N];
int main()
{
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        for(int i = 1; i <= n; ++i)
            scanf("%d %d", &a[i].x, &a[i].y);
        if(n == 1)
        {
            puts("1");
            continue;
        }
        ans = 0;
        for(int k = 1; k <= 1000; ++k)
        {
            int x = rand() % n + 1;
            int y = rand() % n + 1;
            while(x == y)
            {
                x = rand() % n + 1;
                y = rand() % n + 1;
            }
            int res = 2;
            for(int i = 1; i <= n; ++i)
                if(i ^ x && i ^ y)
                    if(1LL * (a[i].y - a[x].y) * (a[i].x - a[y].x) == 1LL * (a[i].y - a[y].y) * (a[i].x - a[x].x))
                        ++res;
            chkmx(ans, res);
        }
        printf("%d\n", ans);
    }
    return 0;
}

P3567 [POI2014]KUR-Couriers

Problem

给一个长度为 \(n\) 的正整数序列 \(a\)。共有 \(m\) 组询问,每次询问一个区间 \([l,r]\) ,是否存在一个数在 \([l,r]\) 中出现的次数严格大于一半。如果存在,输出这个数,否则输出 \(0\)。

\(1 \leq n,m \leq 5 \times 10^5\),\(1 \leq a_i \leq n\)。

Solution

主席树当然是可以的。这里讲随机化做法。

先设计朴素算法:枚举区间内的每个数,统计该数在区间内的出现次数。时间复杂度 \(O(mn^2)\)。

改为 Mante Carlo 算法,随机选择区间内的一个数。

如果询问答案不为 \(0\),则选到正确答案的概率不小于 \(\frac{1}{2}\)。

如果随 \(k\) 次,那么正确率不小于 \(1 - (\frac{1}{2})^{k}\)。

\(O(km\log{n})\)。其中 \(\log{n}\) 可以用 STL 乱搞一下卡过去。

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int n, Q, a[N];
vector<int> vec[N];
int main()
{
    scanf("%d %d", &n, &Q);
    for(int i = 1; i <= n; ++i)
    {
        scanf("%d", &a[i]);
        vec[a[i]].push_back(i);
    }
    while(Q--)
    {
        int l, r;
        scanf("%d %d", &l, &r);
        int times = 19;
        int ans = 0;
        int len = r - l + 1;
        while(times--)
        {
            int pos = l + rand() % len;
            int x = a[pos];
            if((int)vec[x].size() <= len / 2) continue;
            vector<int>::iterator rr = upper_bound(vec[x].begin(), vec[x].end(), r);
            vector<int>::iterator ll = lower_bound(vec[x].begin(), vec[x].end(), l);
            if(rr - ll > len / 2) 
            {
                ans = x;
                break;
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

CF364D Ghd

Problem

定义一个集合的Ghd为 所有大小至少为一半的子集的\(\mathrm{gcd}\) 的最大值。

给定集合\(a\),求\(a\)的Ghd。

Solution

一个想法是从答案上枚举数 \(x\),统计数列中有多少个数是 \(x\) 的倍数,如果 \(\ge \frac{|S|}{2}\) 就更新答案为 \(x\)。(实际上 \(x\) 不一定是符合条件的 ghd)显然答案一定是所有数的因数的集合中的某一个,因此可以从这里面进行枚举。过不了。

另一个想法是从数列中枚举数 \(a_x\),去找集合 \(S^{'}\) 中包含 \(a_x\) 的最大 ghd

记 \(g_i\) 表示 \(a_x\) 与 \(a_i\) 的最大公约数。那么在假定 \(a_x\) 必选的条件下,如果 \(a_i\) 也被选择,那么答案只可能在 \(g_i\) 的因子中出现。

对 \(a_x\) 的每一个因子 \(d\) 记录 \(cnt_d\),表示(最多)有多少个数加进 \(S^{'}\) 内 ghd 可以为 \(d\)。对所有 \(g_i\) 的每一个因子 \(p\),将 \(cnt_p\) 加一。

设 \(\tau(x)\) 为 \(x\) 的因子个数,则算法的时间复杂度为 \(O(n(\sqrt{a_x}+\tau^2(a_x)+nlog{n}))\)。其中 \(\tau^{2}(a_x)\) 是考虑用递推优化时间复杂度。\(10^{12}\) 范围内的因数最多的数是 \(963761198400\),有 \(6720\) 个因子。

现在考虑加上随机化减少枚举次数。不难发现,一个数在答案存在的 \(S^{'}\) 中的概率至少为 \(\frac{1}{2}\),那么枚举 \(10\) 次的正确率至少为 \(\frac{1023}{1024}\)。

code CF364D

TKCONVEX

Problem

Solution

首先考虑如何判断 \(k\) 条边是否能构成一个凸多边形,再考虑 \(2k\) 条边是否能构成两个凸多边形。

假设给定 \(k\) 条边的边长 \(a_1, a_2, \cdots, a_k\),\(C = \sum\limits_{i = 1}^{k}a_i\)。

构成多边形的条件:\(\forall i\in [1, k], i\in \N_{+}\),\(a_i < \frac{C}{2}\)。即 \(\max\limits_{i = 1}^{k}a_i < \frac{C}{2}\)。

由三角形的构成条件可以推广到多边形的构成条件,即一条边的长度小于其他边长度的总和。

\(a_i < C - a_i\),移项可得。

任意非凸多边形都可以通过对其边的平移和旋转得到一个凸多边形。

只能感性理解了(,这条引理说明第一条引理即可判断是否能构成凸多边形。

还是考虑朴素算法,暴力枚举判断,共有 \(\binom{n}{2k}\binom{2k}{k}\) 种情况。

论文又给出了如下定理:

将线段长度排序,若 \(n\) 条线段中存在可以构成凸多边形的大小为 \(k\) 的子集,那么也一定存在一个子集,满足其中的元素在排序后的序列上对应一段区间。

证明

运用了反证法。任选一个可以构成凸多边形的大小为 \(k\) 的子集,记为 \(S\)。记排序后的序列为 \(p\),假设 \(S\) 中的元素对应到 \(p\) 上不是一段区间,则一定存在两个 \(i(i < n)\),满足 \(p_i\in S\) 且 \(p_{i + 1}\not\in S\)。这里要注意的是由于 \(2k \le n\),因此上述才成立。

找到满足上述条件的第二大的 \(i\),并从 \(S\) 中删去 \(p_i\),再加入 \(p_{i + 1}\),由第一条引理,此时 \(\max{a_i}\) 不变,\(C\) 增加,因此仍然满足 \(\max{a_i} < \frac{C}{2}\),能构成凸多边形。重复这样做直到无法操作,发现最后的 \(S\) 中的元素对应到 \(p\) 上就是一段区间。

接下来的思路是什么呢?

首先把原线段集合分成两个集合,然后在这两个集合分别按上述方法将元素从小到大排序后扫一遍,找出符合条件的一段长度为 \(k\) 的区间。注意到时间复杂度的瓶颈在于枚举子集,因此考虑在这上面加入随机化改造成 Monte Carlo 算法。

具体地,假设两个线段集合分别为 \(S\) 和 \(T\),则一个线段均有 \(\frac{1}{2}\) 的概率被分配到 \(S\) 或 \(T\)。那么我们就随机线段的分配情况。每一次随机分配后查找答案的时间复杂度为 \(O(n)\)。

现在来分析算法的正确度如何。总共有 \(2^n\) 种子集划分情况(每个元素有两种归属方式),考察有解的最坏情况,即有且仅有两个大小为 \(k\) 的不相交集合 \(X\) 和 \(Y\) 均满足能构成凸多边形。则一个划分有解,当且仅当 \(X \cap S = X,Y \cap S = \empty\) 或 \(X \cap S = \empty, Y \cap S = Y\)。方案数为 \(2^{n - 2k} \times 2\)。即除了那 \(2k\) 条钦定线段外的线段可以随意分配,且 \(X\) 和 \(Y\) 所在集合可以交换。一次正确的概率为 \(\frac{1}{2^{2k - 1}}\)。

假设随 \(t\) 次,那么总正确率为 \(1 - (1 - \frac{1}{2^{2k - 1}})^t\)。

由于总的时间复杂度为 \(O(n\log{n} + tn)\),因此不要命的话 \(t\) 最大可以取 \(2\times 10^5\)。

当 \(k = 10\) 的时候正确率也不过 \(0.32\)。所以我想说明随机化在骗分上的作用显然更广泛。

正解是 dfs,哈哈。

下为随机化的实现,当个乐子看:六十多个点,这辈子都过不了。

#include<bits/stdc++.h>
#define PII pair<int, int>
#define MP make_pair
#define LL long long
using namespace std;
const int N = 1e3 + 5;
int n, k;
int len[N];
int acnt, bcnt, ida, idb;
PII a[N], b[N];
LL sa, sb, ma, mb;
bool flag = 0;
void work()
{
    flag = 1;
    puts("Yes");
    for(int i = ida; i <= ida + k - 1; ++i)
        printf("%d ", a[i].second);
    for(int i = idb; i <= idb + k - 1; ++i)
        printf("%d ", b[i].second);
}
int main()
{
    srand(time(0));
    scanf("%d %d", &n, &k);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &len[i]);
    sort(len + 1, len + n + 1);
    int times = 200000;
    while(times-- && !flag)
    {
        acnt = bcnt = 0;
        for(int i = 1; i <= n; ++i)
        {
            int x = rand() & 1;
            if(x & 1) a[++acnt] = MP(len[i], i);
            else b[++bcnt] = MP(len[i], i);
        }
        if(acnt < k || bcnt < k) continue;
        ida = idb = 0;
        sa = sb = ma = mb = 0;
        for(int i = 1; i <= k; ++i)
            sa += a[i].first, sb += b[i].first;
        ma = a[k].first, mb = b[k].first;
        if(2 * ma < sa) ida = 1;
        if(2 * mb < sb) idb = 1;
        for(int i = k + 1; i <= acnt && !ida; ++i)
        {
            ma = a[i].first, sa += a[i].first - a[i - k].first;
            if(2 * ma < sa) ida = i - k + 1;
        }
        for(int i = k + 1; i <= bcnt && !idb; ++i)
        {
            mb = b[i].first, sb += b[i].first - b[i - k].first;
            if(2 * mb < sb) idb = i - k + 1;
        }
        if(ida && idb) work();
    }
    if(!flag) puts("No");
    return 0;
}

P1224 [NOI2013] 向量内积

Problem

两个 \(d\) 维向量 \(A=[a_1,a_2,\ldots,a_d]\) 与 \(B=[b_1,b_2,\ldots,b_d]\) 的内积为其相对应维度的权值的乘积和,即:

\[(A,B)=\sum_{i=1}^d a_ib_i=a_1b_1+a_2b_2+\ldots+a_db_d \]

现有 \(n\) 个 \(d\) 维向量 \(x_1,\ldots,x_n\) ,小喵喵想知道是否存在两个向量的内积为 \(k\) 的倍数。请帮助她解决这个问题。

Solution

从判断无解入手降低复杂度

具体地,要判断第 \(i\) 个向量与之前的一堆向量中是否有一组向量点积为 \(2\) 或 \(3\) 的倍数,用这个向量乘之前向量的平方的和,结果为 \(x\)。如果没有方案,则 \(x \equiv (i - 1) \pmod k\)。

分类讨论:

当 \(k = 2\) 时,之前的每个向量与该向量内积均 \(\bmod 2 = 1\),则之前的向量相加后与该向量的内积应为 \((i - 1) \bmod 2\),相当于之前的每一组向量都剩了一个 \(1\),一共剩出 \(i - 1\) 个 \(1\)。反之,若和向量与该向量的内积不等于 \((i - 1) \bmod 2\),一定有解。

当 \(k = 3\) 时,之前的每个向量与该向量内积 \(x\) 均 \(\bmod 3 = 1,2\),则 \(x^2 \bmod 3 = 1\),那么我们判断前 \(i - 1\) 个向量与 \(i\) 的内积的平方和与 \((i - 1) \bmod 3\) 是否同余。分析一下,\((\sum\limits_{x = 1}^{d}a_{i, x}a_{j, x})^2 = \sum\limits_{x = 1}^{d}\sum\limits_{y = 1}^{d}a_{i, x}a_{i, y}a_{j, x}a_{j, y}\)。因此我们维护出 \(b_{x, y} = \sum\limits_{i}{a_{i, x} a_{i, y}}\)。

对于 \(k = 2\),其实也可以用 \(k = 3\) 的方法,只不过时间复杂度上多了个 \(d\) 倍。

然后我们用 random_shuffle 随几次就可以了。

为什么还要随呢?因为被我们判断成无解的情况中也有可能有解,但有解被完全误判的概率由于 random_shuffle 的存在还是比较小的。

但看起来原数据并没有给无解的点?

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 1e5 + 5;
const int M = 105;
int n, d, k;
int a[N][M];
int id[N];
int b[N][M];
int get(int x)
{
    int res = 0;
    for(int i = 1; i <= d; ++i)
        for(int j = 1; j <= d; ++j)
        {
            res += a[x][i] * a[x][j] * b[i][j] % k;
            b[i][j] += a[x][i] * a[x][j] % k;
        }
    return res % k;
}
int check(int i, int j)
{
    int res = 0;
    for(int x = 1; x <= d; ++x)
        res += a[i][x] * a[j][x];
    return res % k;
}
int main()
{
    scanf("%d %d %d", &n, &d, &k);
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= d; ++j)
            scanf("%d", &a[i][j]);
    if(n <= 1000)
    {
        bool flag = 0;
        for(int i = 2; i <= n && !flag; ++i)
            for(int j = 1; j < i && !flag; ++j)
            {
                LL res = 0;
                for(int x = 1; x <= d; ++x)
                    res += 1LL * a[i][x] * a[j][x];
                if(res % k == 0)
                {
                    flag = 1;
                    printf("%d %d\n", j, i);
                }
            }
        if(!flag) puts("-1 -1");
        return 0;
    }
    for(int i = 1; i <= n; ++i)
        id[i] = i;
    int T = 1;
    while(T--)
    {
        memset(b, 0, sizeof(b));
        random_shuffle(id + 1, id + n + 1);
        for(int i = 1; i <= n; ++i)
        {
            int w = get(id[i]);
            if(w == (i - 1) % k)
                continue;
            for(int j = 1; j < i; ++j)
                if(!check(id[i], id[j]))
                {
                    if(id[i] > id[j]) swap(id[i], id[j]);
                    printf("%d %d\n", id[i], id[j]);
                    return 0;
                }
        }
    }
    puts("-1 -1");
    return 0;
}

CF329C Graph Reconstruction

Problem

现有一 \(n\) 个点 \(m\) 条边的简单无向图 \(G\),保证每个点的度数均不超过 \(2\),试求一与 \(G\) 点集相同而边集不相交的简单无向图 \(G'\) 使得 \(G'\) 中每个点度数均不超过 \(2\),且 \(G'\) 的边数与 \(G\) 相同。

无解请输出\(-1\)。

\(1\le m\le n\le 10^5\)

Solution

对于第三个条件,即新图中不含原图中的边,等价于从原图的补图中选一个子图满足条件一和二。

而第二个条件,每个点的度数小于等于 \(2\),容易想到链和环是满足的(且没有其他满足条件的情况),因此新图应该是一些链和环的组合。

又因为原图每个点的度数也小于等于 \(2\),因此补图每个点的度数 \(\ge n - 3\)。

现在在补图中选 \(m\) 条边,求具体方案,若没有则判无解。

首先考虑什么时候可能会无解。

Ore's Theorem:对于一个含 \(n\) 个点的无向图,其存在哈密尔顿回路的一个充分条件是:对于任意两个不相邻的点 \(u, v\),均满足:

\[d_u + d_v \ge n \]

其中 \(d_x\) 表示点 \(x\) 的度数。

放在这道题的补图上,让两个的度数和大于 \(n\),即 \(2(n - 3) \ge n\),解得 \(n\ge 6\)。即对于任意 \(\ge 6\) 的补图,均能找到一个哈密尔顿回路,而哈密尔顿回路显然是合法的。

\(n < 6\) 的情况,我们可以暴力。

于是现在带着必然有解的想法分析 $n \ge 6 $ 的情况。既然有加强限制去找哈密尔顿回路的想法,我们不妨按照这个思路求出具体方案。

在一个给定的图中寻找哈密尔顿回路是 NPC 问题。

没关系随机一下很简单。

随机一个排列,然后按顺序连一个环就行了。最后判断一下是否环内的所有边都没有在原图中出现过。如果是,那么就找到了一组合法解。

我们想知道正确率如何。

考虑随一次,\(f^{n}_{i}\) 表示排列长度为 \(n\) 的情况下,确定的排列的前 \(i\) 位后尚未产生错误的概率。

\(f^{n}_{1} = 1\),一条边都没有,产生错误的概率为 \(0\)。然后转移,对 \(i > 1\):

\[\begin{aligned} f^{n}_i &= f^{n}_{i - 1}\frac{1}{\binom{n - 1}{2}}\left(\binom{i - 2}{2} + (i - 2)(n - i + 1)\frac{n - i}{n - i + 1} + \binom{n - i + 1}{2}\frac{n - i - 1}{n - i + 1} \right) \\ &= \left(\frac{n - 3}{n - 1} \right)^n \end{aligned} \]

需要指出,只是 近似估计,并不考虑实际取了哪些点,只关心一次选择中不产生错误的概率。(不会:(

当 \(n \ge 9\) 时,已经有 \(f^{n}_{n} \ge 0.1\),因此随 \(10\) 次差不多可。

小范围暴力即可。或者多随几次也可以。

code CF329C

标签:信息学,竞赛,frac,int,复杂度,ge,算法,向量
From: https://www.cnblogs.com/Schucking-Sattin/p/17032095.html

相关文章

  • 代码随想录算法训练营第10天 | 232. 用栈实现队列 225. 用队列实现栈
    232.用栈实现队列文章:代码随想录(programmercarl.com)思路:使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈......
  • 算法刷题 Day 10 | 232.用栈实现队列 225. 用队列实现栈
    今日任务:理论基础用栈实现队列用队列实现栈理论基础了解一下栈与队列的内部实现机智,文中是以C++为例讲解的。文章讲解:https://programmercarl.com/%E6%A0%......
  • 道长的算法笔记:状态机模型之股票系列问题
    (一)股票系列问题所谓的股票问题,是一个动态规划状态机模型的系列问题,这些题目来自于LeetCode社区,这些问题非常经典,能够帮助我们理解动态规划的本质,这些问题大多初看之......
  • Dijkstra(迪杰斯特拉)算法C++实现&讲解
    Dijkstra迪杰斯特拉算法及C++实现Dijkstra算法是典型的最短路径路由算法,用来计算一个节点到其他所有节点的最短路径。算法的基本思想和流程是:1.初始化出发点到其它各点的......
  • Raft一致性共识算法论文学习
    论文地址:https://pdos.csail.mit.edu/6.824/papers/raft-extended.pdf看完raft共识算法,脑袋非常懵,所以写一篇学习笔记,记录一下。raft算法主要解决三个模块的问题:领导人选......
  • TEE非对称加解密算法RSA加密和解密开发实例
    /***自动分配存放秘钥对象**/TEE_Resultlge_utils_generate_keypair(TEE_ObjectHandle*rsa_key_obj){TEE_Resultret;ret=TEE_AllocateTransient......
  • 1-k-近邻算法
    title:1-k-近邻算法date:2021-01-1810:58:30permalink:/pages/1699dd/......
  • 深入浅出回溯算法
    一,如何理解回溯算法深度优先搜索算法利用的就是回溯算法思想,但它除了用来指导像深度优先搜索这种经典的算法设计之外,还可以用在很多实际的软件开发场景中,比如正则表达式匹......
  • 竞赛图
    竞赛图,是每两个点之间都有一条有向边的图。因为有一种模型是两者之间比赛谁能赢,赢的人对输的人连一条边,因此叫做竞赛图。竞赛图的性质:对SCC缩点之后是一条链式结构,类......
  • C++ 不知树系列之二叉堆排序(递归和非递归实现上沉、下沉算法)
    1.前言什么是二叉堆?二叉堆是有序的完全二叉树,在完全二叉树的基础上,二叉堆提供了有序性特征:二叉堆的根结点上的值是整个堆中的最小值或最大值。当根结点上的值......