peiwenjun's blog 没有知识的荒原

P5208 [WC2019] I 君的商店 题解

题目描述

\(T\) 组数据,有 \(n\)\(01\) 变量,初始值未知,保证至少存在一个 \(1\)

交互库初始会告诉你 \(1\) 的个数的奇偶性,你的目标是求出哪些变量的值是 \(1\)

你需要实现以下函数:

  • find_price(int id,int n,int k,int *ans)

    其中 \(id\) 为子任务编号, \(n\) 为数组长度, \(k\in\{0,1\}\) 表示 \(1\) 的个数奇偶性,你需要将每个变量的值放在 \(ans\) 数组中。

你可以调用以下函数:

  • int query(int *S,int nS,int *T,int nT)

    你可以传入两个不交集合,然后交互库会告诉你哪个集合 \(1\) 的个数更多。

    如果两个集合 \(1\) 的个数相等,交互库会按照自己的方式选择一个集合告诉你。

显然你不能无限询问下去,因此交互库设定了集合大小( \(\sum (|S|+|T|)\) )的上限 \(m\) ,你不能违反这个限制。

数据范围

  • \(id=1\)\(1\le n\le 5,m=100\)
  • \(id=2\)\(1\le n\le 10^3,m=10^6\)
  • \(id=3\)\(1\le n\le 10^5, m=100\) ,保证 \(ans\) 数组单调。
  • \(id=4\)\(1\le n\le 5\cdot 10^4,m=350100\)
  • \(id=5\)\(1\le n\le 10^5,m=500100\)

分析

宣传: 一篇有趣的题解 ,本文在一定程度上参考了这篇文章。

算法一

先看子任务 \(3\) ,显然目标是二分出边界点。

先比较 \(x_1\)\(x_n\) ,如果 \(x_1\le x_n\) 就翻转整个序列,从而把 \(1\) 放前面, \(0\) 放后面。

由于题目保证至少有一个 \(1\) ,所以 \(x_1=1\)

每次拿相邻两个数与 \(x_1\) 比较:

\(x_i\) \(x_{i+1}\) \(query(\{x_i,x_{i+1}\},\{1\})\)
1 1 1
1 0 0,1 都有可能
0 0 0

先正常二分,假设 query({l,l+1},1)=1,query({r,r+1},1)=0,r-l=1 ,那么我们能确定 \(x[1\sim l]=1\)\(x[r+1\sim n]=0\) ,但 \(x_r\) 无法确定。

但是别忘了奇偶性! \(x_r\) 可以通过奇偶性反推出来。

于是我们用 \(3\log n+2\) 的代价解决了子任务 \(3\) ,期望得分 \(9pts\)

算法二

根据数据范围,大胆猜测子任务 \(3\) 为本题的最后一个步骤。

再来看子任务\(1,2\),目标变为\(\mathcal O(n^2)\)的时间内将所有元素倒序排序

将所有数两两比较一次。

Key observation:按照每个数 \(\ge\) 其他数的次数排序即可!

这是因为,如果 \(x_i=1,x_j=0\) ,总共有 \(t\)\(0\) ,那么 \(x_i\) 至少 \(\ge t\) 个数,而 \(x_j\) 至多\(\ge t-1\) 个数。

总代价 \(n(n-1)+3\log n+2\) ,期望得分 \(40pts\)

#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=1e5+5;
int p[maxn],s[maxn],t[maxn];
pii v[maxn];
int query(int*,int,int*,int);
void find_price(int id,int n,int k,int *ans)
{
    for(int i=0;i<n;i++) p[i]=i;
    if(id<=2)
    {
        for(int i=0;i<n;i++) v[i]=mp(0,i);
        for(int i=0;i<n;i++)
            for(int j=i+1;j<n;j++)
                s[0]=i,t[0]=j,v[!query(s,1,t,1)?i:j].fi++;
        sort(v,v+n,greater<pii>());
        for(int i=0;i<n;i++) p[i]=v[i].se;
    }
    else
    {
        s[0]=0,t[0]=n-1;
        if(query(s,1,t,1)) reverse(p,p+n);
    }
    int l=0,r=n-1;
    while(r-l>1)
    {
        int mid=(l+r)/2;
        s[0]=p[mid],s[1]=p[mid+1],t[0]=p[0];
        if(query(s,2,t,1)) r=mid;
        else l=mid;
    }
    for(int i=0;i<=l;i++) ans[p[i]]=1;
    for(int i=r+1;i<n;i++) ans[p[i]]=0;
    ans[p[r]]=k-r&1;
}
算法三

\(\mathcal O(n^2)\) 的排序显然不优,但是询问 \(2\) 个数本质就是比较大小,如果两数不同则可以得到正确结果,如果相同虽然结果随机,但不影响排序。

随便换成一个 \(\mathcal O(n\log n)\) 的排序即可通过子任务 \(4\) ,期望得分 \(52pts\)

算法四

基于比较的排序下界复杂度为 \(\mathcal O(n\log n)\) ,因此上述算法无法继续优化。

题目保证了至少有一个 \(1\) ,扫描整个数组可以用 \(2n\) 的代价把最大值(必然是 \(1\) )找出来。

接下来执行以下算法:

  • 如果只有 \(1\) 个数没有确定,用奇偶性解决。

  • 否则至少有 \(2\) 个数 \(x,y\) 没有确定。

    先对 \(x,y\) 比较大小,不妨 \(x\ge y\)

    再比较 \(\{x,y\}\) 和最大值的大小,有三种情况:

    • \(x=1,y=1\)\(ans=0\)
    • \(x=1,y=0\)\(ans=0/1\)
    • \(x=0,y=0\)\(ans=1\)

    换言之:

    • \(ans=0\) ,则 \(x=1\)
    • \(ans=1\) ,则 \(y=0\)

    因此我们可以用 \(5n\) 的代价确定一个数!

算上前面找最大值的开销,总代价 \(7n\),期望得分 \(60pts\) (没拼子任务 \(3\) )。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int s[maxn],t[maxn];
queue<int> q;
int query(int*,int,int*,int);
void find_price(int id,int n,int k,int *ans)
{
    int p=0;
    for(int i=1;i<n;i++)
    {
        s[0]=p,t[0]=i;
        if(query(s,1,t,1)) p=i;
    }
    k^=1,ans[p]=1;
    for(int i=0;i<n;i++) if(i!=p) q.push(i);
    while(q.size()>=2)
    {
        int u=q.front();
        q.pop();
        int v=q.front();
        q.pop();
        s[0]=u,t[0]=v;
        if(query(s,1,t,1)) swap(u,v);
        s[0]=u,s[1]=v,t[0]=p;
        if(query(s,2,t,1)) ans[v]=0,q.push(u);
        else k^=1,ans[u]=1,q.push(v);
    }
    if(!q.empty()) ans[q.front()]=k,q.pop();
}
算法五

\(7n\)\(5n\) ,很明显,我们不能直接把最大值求出来了。

任选一个数 \(v\) 代替最大值,然后比较 \(\{x,y\}\)\(v\) 的大小(不妨 \(x\ge y\) ):

  • \(ans=0\) ,即 \(x+y\ge v\) ,因此 \(x\ge v\) 。记录这组偏序关系,然后用 \(x\) 替换 \(v\) ,再找一个新数放在 \(x\) 的位置继续比较。
  • \(ans=1\) ,则 \(x+y\le v\) ,因此 \(y=0\) ,再找一个新数放在 \(y\) 的位置继续比较。

这样我们能得到什么?能得到一堆 \(0\) ,一条偏序关系构成的链,和一个孤立点 \(p\) (如果有 \(\ge 2\) 个孤立点就可以继续比较)。

\(x\) 为链顶,注意我们还没用到最大值为 \(1\) 这个条件,而最大值只可能在 \(x\)\(p\) 之间产生。

\(x\)\(p\) 做一次比较,可以得到最大值。

  • 如果 \(p=1\) ,把 \(p\) 接在链顶,然后套用子任务 \(3\) 的做法。

  • 如果 \(x=1\) ,根据子任务 \(3\) ,二分后链上最多只有一个点 \(q\) 的值不确定。

    回顾子任务 \(4\) ,拿 \(\{p,q\}\)\(1\) 比较可以确定其中一个数,另一个用奇偶性解决。

总代价 \(5n+3\log n+\mathcal O(1)\)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int c[maxn],s[maxn],t[maxn];
int query(int *,int ,int *,int );
int cmp(int x,int y)
{
    s[0]=x,t[0]=y;
    return query(s,1,t,1);
}
int calc(int x,int y,int v)
{
    s[0]=x,s[1]=y,t[0]=v;
    return query(s,2,t,1);
}
void find_price(int id,int n,int k,int *ans)
{
    if(n<=2||id==3)
    {
        for(int i=0;i<n;i++) c[i]=i;
        if(cmp(0,n-1)) reverse(c,c+n);
        int l=0,r=n-1;
        while(r-l>1)
        {
            int mid=(l+r)/2;
            if(calc(c[mid],c[mid+1],c[0])) r=mid;
            else l=mid;
        }
        for(int i=0;i<=l;i++) ans[c[i]]=1;
        for(int i=r+1;i<n;i++) ans[c[i]]=0;
        ans[c[r]]=k-r&1;
        return ;
    }
    int m=1,x=-1,y=0,v=1;
    c[1]=v;
    for(int i=2;i<n;i++)
    {
        if(x==-1) x=i;
        if(y==-1) y=i;
        if(cmp(x,y)) swap(x,y);
        if(calc(x,y,v)) ans[y]=0,y=-1;
        else c[++m]=x,v=x,x=-1;
    }
    int p=max(x,y);
    if(cmp(v,p)) c[++m]=p,p=-1;
    reverse(c+1,c+m+1);
    int l=1,r=m;
    while(r-l>1)
    {
        int mid=(l+r)/2;
        if(calc(c[mid],c[mid+1],c[1])) r=mid;
        else l=mid;
    }
    for(int i=1;i<=l;i++) ans[c[i]]=1;
    for(int i=r+1;i<=m;i++) ans[c[i]]=0;
    if(r!=1)
    {
        int q=c[r];
        if(p==-1) p=q;
        else
        {
            if(cmp(p,q)) swap(p,q);
            if(calc(p,q,c[1])) ans[q]=0;
            else ans[p]=1,p=q;
        }
    }
    if(p!=-1)
    {
        for(int i=0;i<n;i++) if(i!=p) k^=ans[i];
        ans[p]=k;
    }
}

posted on 2022-06-20 00:29  peiwenjun  阅读(8)  评论(0)    收藏  举报

导航