[骗分技巧——随机化Ⅱ] [Poi2014]Couriers,CodeChef - TKCONVEX

本文介绍了如何通过随机算法解决Poi2014 Couriers问题,利用概率分析减少判断次数,并展示了如何应用随机划分技巧在CodeChef-TKCONVEX问题中快速找到凸多边形子集。通过理论推导和实例演示,探讨了如何利用随机策略提高算法效率。
摘要由CSDN通过智能技术生成


随机算法的典型套路:枚举太花时,转化为随机一个数。然后通过对正确率的分析,选择一个随机的次数来卡。前提是要保证每一次检验随机是否为答案的时间复杂度要能够接受。一般不超过 n log ⁡ n n\log n nlogn

[Poi2014]Couriers

problem

BZOJ 3524

solution

如果有解,也就是说区间内的随机选择一个数是答案的概率不大于 1 2 \frac 1 2 21。否则怎么随机都无解。

可以预处理相同值的数出现位置,二分查找求出某个值在 [ l , r ] [l,r] [l,r] 区间的出现次数。

所以一次判断是 O ( log ⁡ n ) O(\log n) O(logn) 的。

随机 20 20 20 次,正确率就在 1 − 1 0 − 5 1-10^{-5} 1105 左右了。

code

#include <bits/stdc++.h>
using namespace std;
#define maxn 500005
int n, m;
int a[maxn];
vector < int > pos[maxn];

int find_l( int x, int p ) { 
    int l = 0, r = pos[x].size() - 1, ans;
    while( l <= r ) {
        int mid = l + r >> 1;
        if( pos[x][mid] >= p ) ans = mid, r = mid - 1;
        else l = mid + 1;
    }
    return ans;
}

int find_r( int x, int p ) {
    int l = 0, r = pos[x].size() - 1, ans;
    while( l <= r ) {
        int mid = l + r >> 1;
        if( pos[x][mid] <= p ) ans = mid, l = mid + 1;
        else r = mid - 1;
    }
    return ans;
}

int main() {
    scanf( "%d %d", &n, &m );
    for( int i = 1;i <= n;i ++ ) {
        scanf( "%d", &a[i] );
        pos[a[i]].push_back( i );
    }
    mt19937 wwl(time(0));
    for( int i = 1, l, r;i <= m;i ++ ) {
        scanf( "%d %d", &l, &r );
        uniform_int_distribution < int > range( l, r );
        for( int k = 1;k <= 20;k ++ ) {
            int x = range( wwl ); x = a[x];
            int L = find_l( x, l );
            int R = find_r( x, r );
            if( R - L + 1 << 1 > r - l + 1 ) { 
                printf( "%d\n", x ); 
                goto next_turn; 
            }
        }
        printf( "0\n" );
        next_turn:;
    }
    return 0;
}

CodeChef - TKCONVEX

problem

vjudge链接

solution

定理1 能构成多边形的线段,必须满足任意一条边长度都小于剩余所有边长度的总和。

这是显然的,因为要形成封闭的图形,可以看作是这条边是最后一条连接两个端点的直线段,其余线段绕成了一个弧形。

事实上只需要长度最长的线段满足这个性质即可。

此定理只能保证构成多边形,但不保证是凸多边形。所以提出下一个定理。

定理2 任意非凸多边形都可以通过对边的旋转和平移构成一个新凸多边形。

在本题中,可以选择的集合为 ( n 2 k ) ( 2 k k ) \binom{n}{2k}\binom{2k}{k} (2kn)(k2k),已经是非常大了,不可能一个一个检验。

定理3 假设有解,那么一定存在一个解(大小为 k k k 的子集)满足:对所有线段长度排序后,这个子集的元素对应排序后序列的一段连续区间。

证明:显然。假设子集为 S S S,排序后的序列为 P P P

如果其对应的元素构成不连续的区间。即 ∃ i P i ∈ S ∧ P i + 1 ∉ S \exist_iP_i\in S\wedge P_{i+1}\notin S iPiSPi+1/S

找到满足条件的 i i i,则 i i i 肯定是一段连续区间的右端点,保证 i i i 不是若干个不连续区间的最右端点。

重复操作移除 P i P_i Pi,加入 P i + 1 P_{i+1} Pi+1。直到将左边的若干个不连续区间和最右边的一个连续区间拼接成一整个大连续区间。

这样操作后,可以发现长度最大的依然是最右边的端点,没有移动过,但是其余线段的长度都有所增长。

既然原来都是符合条件的子集,那么现在肯定也是一个符合条件的子集。

题目要求两个大小为 k k k 的凸多边形。

不妨设第一个凸多边形从边集合为 S S S 中,选择另一个凸多边形从边集合 S ˉ \bar{S} Sˉ 中选择。

满足 S ⋂ S ˉ = ∅ ∧ S ⋃ S ˉ = Ω S\bigcap\bar{S}=\empty\wedge S\bigcup \bar{S}=\Omega SSˉ=SSˉ=Ω

那么对于每一条线段,有 1 2 \frac 1 2 21 的概率属于 S S S 1 2 \frac 1 2 21 的概率属于 S ˉ \bar{S} Sˉ。换言之,选出任意一个子集的概率是相同的。

那么就对每条边随机其属于哪个集合。

随机划分集合后, O ( n ) O(n) O(n) 的可以做到快速判断这个集合能否选出一个子集构成凸多边形。

最后来分析一下这个随机的正确率。

划分的总方案数为 2 n 2^n 2n。考虑极端最坏情况,有且仅有两个互不相交的集合 X , Y X,Y X,Y 可以构成凸多边形。

那么一个随机划分方案有解,当且仅当 S ⋂ X = X ∧ S ⋂ Y = ∅ S\bigcap X=X\wedge S\bigcap Y=\empty SX=XSY= 或者 S ⋂ Y = Y ∧ S ⋂ X = ∅ S\bigcap Y=Y\wedge S\bigcap X=\empty SY=YSX=

这样的方案数为 2 n − 2 k × 2 2^{n-2k}\times 2 2n2k×2

因此在最坏情况下选出一个合法划分的概率为 1 2 2 k − 1 \frac{1}{2^{2k-1}} 22k11

设随机运行以上算法 t t t 次,那么 t t t 最多取到 20000 20000 20000 左右,时间复杂度为 O ( n log ⁡ n + t n ) O(n\log n+tn) O(nlogn+tn)

k = 6 k=6 k=6 时,正确率为 1 − ( 1 − 1 2 11 ) t ≈ 1 − 1 0 − 5 1-(1-\frac{1}{2^{11}})^t≈1-10^{-5} 1(12111)t1105

注意:原题目 k ∈ [ 3 , 10 ] k\in[3,10] k[3,10]。这个随机算法是不能通过的。所以下面的代码不保证正确性。

code

#include <bits/stdc++.h>
using namespace std;
#define maxn 1005
#define int long long
struct node { int val, id; }a[maxn];
int n, k;
int s[2][maxn];
pair < int, int > ans[2];

signed main() {
    scanf( "%lld %lld", &n, &k );
    for( int i = 1;i <= n;i ++ ) scanf( "%lld", &a[i].val ), a[i].id = i;
    sort( a + 1, a + n + 1, []( node x, node y ) { return x.val < y.val; } );
    mt19937 wwl;
    uniform_int_distribution < int > range( 0, 1 );
    for( int t = 1;t <= 20000;t ++ ) {
        s[0][0] = s[1][0] = 0;
        for( int i = 1;i <= n;i ++ ) {
            int x = range( wwl );
            s[x][++ s[x][0]] = i;
        }
        int cnt = 0;
        for( int j = 0;j <= 1;j ++ ) {
            for( int i = 1, len = 0, sum = 0;i <= s[j][0];i ++ ) {
                len ++, sum += a[s[j][i]].val;
                if( len > k ) sum -= a[s[j][i - k]].val;
                if( len == k and sum > (a[s[j][i]].val << 1) ) {
                    ans[j] = make_pair( i - k + 1, i );
                    cnt ++;
                    break;
                }
            }
        }
        if( cnt == 2 ) goto yes;
    }
    no : return ! printf( "No\n" );
    yes :
    printf( "Yes\n" );
    for( int i = 0;i <= 1;i ++ )
        for( int j = ans[i].first;j <= ans[i].second;j ++ )
            printf( "%lld ", a[s[i][j]].id );
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值