2020 CCPC Wannafly Winter Camp Day7 部分题解(AFGHJKL)

查看题目


A 序列

题意:

给一个1到n的排列p,对于一个整数k,该排列的某个子序列的得分为:子序列中每对相邻的数字组成的开区间若包含k则得分加1。
先询问对于k从1取到n,这个排列的所有子序列的得分和为多少。

解题思路:

这种解决“所有子序列的xx之和”之类的问题显然要从每个单元对整个排列的贡献方面来考虑。
对于这题,也就是要考虑每对i,j,计算其对k取哪些值有贡献,对于一对i,j,假设i(i-1)+(n-j)。
但是如果直接暴力枚举i,j,然后用线段树维护其对哪些k有贡献,复杂度是O(n2logn)的,T飞。
这类“看起来要枚举i,j算贡献”题的另一个套路又体现在这题里了,我们可以只枚举一边,然后用某些数据结构来解决问题。
对于这题(为方便考虑,假设ip[j]的情况可以将数组反过来再算一遍),我们先枚举左侧的i,它会与在其右侧且p[j]>p[i]的 j 对[p[i]+1, p[j]-1]之间的k产生相同的贡献(2i-1*2n-j),注意到虽然区间右侧不同,但是区间左侧都是从p[i]开始的,这是开始贡献的情况。枚举右侧的j,找到在其左侧且p[i] 因此我们可以采用差分的方法计算答案,算出k为x和k为x-1时两者的差距delta,就可以维护答案了。具体而言,枚举左侧i时,对在其右侧符合要求的 j 的贡献求和(可以用树状数组,从右往左枚举i,枚举完某点后把这个点作为右端的 2n-j 加到树状数组上),然后贡献和到delta[p[i]+1]上。枚举右侧j时,对在其左侧符合要求的 i 的贡献求和(树状数组,从左往右枚举j),然后贡献和到delta[p[j]-1]上。
这是只考虑p[i]

#pragma GCC optimize(3)
#include 
#define inf 500000
#define Inf 223372036854775807
#define maxn 200100
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
const double eps=1e-10;
const double PI=acos(-1.0);
 
ll n, num[maxn], mi[maxn]={1};
ll delta[maxn];
ll tr[maxn+5];
 
void add(ll x,ll v)
{
    for (ll i=x; i<maxn; i+=(i&-i))
        tr[i]=(tr[i]+v)%mod;
}
ll query(ll x)
{
    ll ans=0;
    for (ll i=x; i>0; i-=(i&-i))
        ans=(ans+tr[i])%mod;
    return ans;
}
 
void solve()
{
    memset(tr, 0, sizeof(tr));
    for (int i=n; i>=1; i--)
    {
        ll q=(query(n)-query(num[i]+1)+mod)%mod;
        q=q*mi[i-1]%mod;
        delta[num[i]+1]=(delta[num[i]+1]+q)%mod;
        add(num[i], mi[n-i]);
    }
    memset(tr, 0, sizeof(tr));
    for (int i=1; i<=n; i++)
    {
        if (num[i]>2)
        {
            ll q=query(num[i]-2);
            q=q*mi[n-i]%mod;
            delta[num[i]]=(delta[num[i]]-q+mod)%mod;
        }
        add(num[i], mi[i-1]);
    }
}
 
int main()
{
    cin>>n;
    for (int i=1; i<=n; i++)
    {
        cin>>num[i];
        mi[i]=mi[i-1]*2%mod;
    }
    solve();
    reverse(num+1, num+1+n);
    solve();
    ll ans=0;
    for (int i=1; i<=n; i++)
    {
        ans=(ans+delta[i])%mod;
        cout<<ans<<"\n";
    }
    return 0;
}

F 草莓

题意:

农场为一个n*m的棋盘,wls初始位于第x行第y列,初始每个格子没草莓,每天早上每个格子长一个草莓,每天下午wls可以上下左右移动一次(或不动),每天晚上收获当前格子草莓,问k天后最多收多少草莓。

解题思路:

假设被走过的格子总数为x,答案上界就是这些格子里最后剩下的草莓为0,1,2…x-1。
假设行比列少(即n 当n>1时一定可以达到上界(k>n*m一直停在原地到最后n*m天走一个哈密顿路径,k 当n==1时就需要仔细考虑,若k小于y到两端距离长的那一段(假设到左端比较近),则向右直接走出k个格子的长度最优,若大于到右端距离则可以先向左走几步,然后向右一直走到最右点最优,若可以向左走到头在向右走到头则可以走满m个格子,这其中细节的把控可以参考代码。

#pragma GCC optimize(3)
#include 
#define inf 1000000000
#define Inf 223372036854775807
#define maxn 1001000
using namespace std;
typedef long long ll;
const ll mod=998244353;
const long double eps=1e-9;
const long double PI=acos(-1.0);

ll T, n, m, x, y, k, inv=499122177;

ll cal(ll l, ll r)
{
    l%=mod, r%=mod;
    return (l+r)*((r-l+mod+1)%mod)%mod*inv%mod;
}

int main()
{
    cin>>T;
    while (T--)
    {
        cin>>n>>m>>x>>y>>k;
        if (n>m) swap(n, m), swap(x, y);
        if (n>1)
        {
            if (k>=n*m) cout<<cal(k-n*m+1, k)<<"\n";
            else cout<<cal(1, k)<<"\n";
        }
        else
        {
            ll sho=max(min(y-1, m-y)-1, 0ll), len;
            if (k<=m-sho) len=k;
            else len=m-sho+(k-m+sho)/2;
            if (k>=sho+m) len=m;
            cout<<cal(k-len+1, k)<<"\n";
        }
    }
    return 0;

}


G 草莓2

题意:

和F题类似,不过每天早上收草莓,每天下午wls可以上下左右移动一次(或不动),每天晚上长草莓,问k天后最多收多少草莓。
nm范围小且一定为偶数,初始每个格子里有草莓。

解题思路:

kn*m时因为n和m都是偶数,所以一直走哈密顿回路,答案一定是最优的。

#pragma GCC optimize(3)
#include 
#define inf 1000000000
#define Inf 223372036854775807
#define maxn 5010
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
const long double eps=1e-9;
const long double PI=acos(-1.0);

ll n, m, x, y, k, ans, a[20][20];
ll nxt[5][2]={-1, 0, 1, 0, 0, -1, 0, 1, 0, 0};

void dfs(ll nx, ll ny, ll res, ll t)
{
    res+=a[nx][ny]+t-1;
    if (t==k) {ans=max(ans, res); return ;}
    ll buf=a[nx][ny];
    a[nx][ny]=0;
    ll tx, ty;
    for (ll i=0; i<5; i++)
    {
        tx=nx+nxt[i][0], ty=ny+nxt[i][1];
        if (tx<1 || tx>n || ty<1 || ty>m) continue;
        dfs(tx, ty, res, t+1);
    }
    a[nx][ny]=buf;
}

ll solve2()
{
    ll res=0;
    for (ll i=1; i<=n; i++)
        for (ll j=1; j<=m; j++)
            res+=a[i][j];
    res+=n*m*(n*m-1)/2;
    k-=n*m;
    res+=k*n*m;
    return res;
}

int main()
{
    cin>>n>>m>>x>>y>>k;
    for (ll i=1; i<=n; i++)
        for (ll j=1; j<=m; j++)
            cin>>a[i][j];
    if (k<n*m)
        dfs(x, y, 0, 1);
    else ans=solve2();
    cout<<ans;
    return 0;
}


H 游戏

题意:

n个数,每次当剩余数的数量大于1时随机取两个数删掉,若这两个数互质则得一分,问最后得分的期望值。

解题思路:

每一个数对被取到的概率相等,因此统计有几对数互质就可以算出期望。

#pragma GCC optimize(3)
#include 
#define inf 1000000000
#define Inf 223372036854775807
#define maxn 5010
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
const long double eps=1e-9;
const long double PI=acos(-1.0);

ll n, fz, fm;

int main()
{
    cin>>n;
    for (ll i=1; i<=n; i++)
        for (ll j=i+1; j<=n; j++)
            if (__gcd(i, j)==1) fz++;
    fz*=n/2, fm=(n-1)*n/2;
    if (n!=1) cout<<fz/__gcd(fz, fm)<<"/"<<fm/__gcd(fz, fm)<<endl;
    else cout<<"0/1"<<endl;
    return 0;
}


J King

题意:

向下取整符号不会打orz,点这里看题

解题思路:

对于序列中第二个及以后的位置i都有一个从胖子串的串首的到的j的一个取值范围:( a[i]/(a[i-1]+1), (a[i]+1)/a[i-1] ),因此只要枚举i,把它当成胖子串的串首,然后二分串尾。用线段树维护每个点的这个区间,合并的时候左端点取较大者,右端点取较小者。check的时候检查串首到串尾间点区间合并后左端是否小于右端,串首能得到的j是否在与区间有交即可检验是否合法。

#pragma GCC optimize(3)
#include 
#define inf 500000
#define Inf 223372036854775807
#define maxn 100100
using namespace std;
typedef long long ll;
const ll mod=998244353;
const double eps=1e-10;
const double PI=acos(-1.0);

int n, ans=1;
double a[maxn], mij[maxn], mxj[maxn];
struct Seg
{
    double mi, mx;
}tr[maxn<<2];

Seg pushup(Seg x, Seg y)
{
    Seg temp;
    temp.mi=max(x.mi, y.mi);
    temp.mx=min(x.mx, y.mx);
    return temp;
}

void build(int l=2, int r=n, int pos=1)
{
    if (l==r)
    {
        tr[pos].mi=mij[l], tr[pos].mx=mxj[l];
        return ;
    }
    int mid=(l+r)/2;
    build(l, mid, pos<<1), build(mid+1, r, pos<<1|1);
    tr[pos]=pushup(tr[pos<<1], tr[pos<<1|1]);
}

Seg query(int L, int R, int l=2, int r=n, int pos=1)
{
    if (l>=L && r<=R) return tr[pos];
    int mid=(l+r)/2;
    if (R<=mid) return query(L, R, l, mid, pos<<1);
    else if (L>mid) return query(L, R, mid+1, r, pos<<1|1);
    else return pushup(query(L, R, l, mid, pos<<1), query(L, R, mid+1, r, pos<<1|1));
}

int check(int fi, int ov)
{
    Seg temp=query(fi+1, ov);
    if (temp.mi>temp.mx+eps) return 0;
    if (temp.mi>a[fi]+1+eps || temp.mx<a[fi]-eps) return 0;
    return 1;
}

int main()
{
    cin>>n;
    for (int i=1; i<=n; i++)
        cin>>a[i];
    for (int i=2; i<=n; i++)
        mij[i]=a[i]/(a[i-1]+0.999999), mxj[i]=(a[i]+0.999999)/a[i-1];
    if (n>1) build();
    for (int i=1; i<n; i++)
    {
        int l=i+1, r=n, mid;
        while (l<r)
        {
            mid=(l+r)/2;
            if (check(i, mid)) l=mid+1;
            else r=mid;
        }
        if (!check(i, l)) l--;
        ans=max(ans, l-i+1);
    }
    cout<<ans;
    return 0;
}


K 修炼

题意:

有两个人物,其初始能力值为v1,v2,此外每天还会分别提升a1,a2的能力,此外每天在能力提升前还可以选择让a1增加1或让a2增加1。
给出q对b1,b2,分别输出对于每对b要使v1>=b1, v2>=b2需要的最少天数。

解题思路:

首先,题目符合二分的性质,可以二分天数。
对k天进行check看是否符合要求,v1至少会增加k*a1的能力,v2至少会增加k*a2的能力,此外在第i天选择让a1增加1或让a2增加1等价与让某个人最终能力增加k-i+1(因为剩余的每天都会多得到一点能力)。因此只要计算b1-k*a1和b2-k*a2(小于0则取0)之和是否小于 (1+k)*k/2即可。

#pragma GCC optimize(3)
#include 
#define inf 1000000000
#define Inf 223372036854775807
#define maxn 5010
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
const long double eps=1e-9;
const long double PI=acos(-1.0);

ll n, a1, a2, b1, b2, ans=inf;

ll check(ll k)
{
    ll B1=b1-k*a1, B2=b2-k*a2;
    B1=max(B1, 0ll), B2=max(B2, 0ll);
    if (k*(k+1)/2>=B1+B2) return 1;
    return 0;
}

int main()
{
    cin>>a1>>a2>>n;
    for (int i=1; i<=n; i++)
    {
        cin>>b1>>b2;
        ll l=1, r=inf, mid;
        while (l<r)
        {
            mid=(l+r)/2;
            if (check(mid)) r=mid;
            else l=mid+1;
        }
        ans=min(ans, l);
    }
    cout<<ans;
}


L 图

题意:

n个点(n<=20)的图,初始每个点都有黑色或白色,下一轮点的颜色取决与连向它的点有几个为黑色,若有奇数个,则这个点下一轮为黑,偶数个则下轮为白。
q次询问,每次询问给出x,k。输出x号点至少要到多少轮才能至少出现过k次黑色

解题思路:

点很少,可以用状压表示图的状态。
容易观察出,经过一定轮数后(也可能一开始就处于循环)一定会进入循环状态,我们只要找到在哪里开始循环,记录一下过程中出现黑色的次数,求答案的时候稍微搞搞就ok了。
我的过程是先判断是否无解(即循环内x号点不出现黑色且进入循环前也不够k次黑色),然后判断是否进入循环。
未进入循环直接二分位置。
若进入循环,可以先判断循环了几次,然后再二分找最后一次在循环里的位置。

#pragma GCC optimize(3)
#include 
#define inf 1000000000
#define Inf 223372036854775807
#define maxn 1001000
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
const long double eps=1e-9;
const long double PI=acos(-1.0);

vector<int> edge[25];
int vis[maxn<<2];
int n, m, q;
int cnt=1, col[maxn][22], loop[22], loopst;

int get_col(int k)
{
    for (int i=1; i<=n; i++)
        for (auto j: edge[i])
            col[k][i]+=col[k-1][j];
    for (int i=1; i<=n; i++)
        col[k][i]&=1;
    int sta=0;
    for (int i=1; i<=n; i++)
        if (col[k][i]) sta|=(1<<i);
    return sta;
}

int find_(int x, int k)
{
    int l=1, r=cnt, mid;
    while (l<r)
    {
        mid=(l+r)/2;
        if (col[mid][x]>=k) r=mid;
        else l=mid+1;
    }
    return l;
}

int main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    cin>>n>>m>>q;
    for (int i=1; i<=n; i++)
        cin>>col[1][i];
    int sta=0;
    for (int i=1; i<=n; i++)
    {
        if (col[1][i]) sta|=(1<<i);
    }
    vis[sta]=1;
    for (int i=1, u, v; i<=m; i++)
    {
        cin>>u>>v;
        edge[v].push_back(u);
    }
    while (true)
    {
        int sta=get_col(++cnt);
        if (vis[sta]) {cnt--, loopst=vis[sta]; break;}
        vis[sta]=cnt;
    }
    for (int i=loopst; i<=cnt; i++)
        for (int j=1; j<=n; j++)
            loop[j]+=col[i][j];
    for (int i=1; i<=cnt; i++)
        for (int j=1; j<=n; j++)
            col[i][j]+=col[i-1][j];
    for (int qq=1; qq<=q; qq++)
    {
        int x; ll k;
        cin>>x>>k;
        if (k==0)
        {
            cout<<0<<"\n";
            continue;
        }
        if (k>col[cnt][x] && loop[x]==0)
        {
            cout<<-1<<"\n";
            continue;
        }
        if (k<=col[cnt][x])
        {
            cout<<find_(x, k)-1<<"\n";
        }
        else
        {
            ll res=0;
            k-=col[loopst-1][x];
            if (k%loop[x]==0)
            {
                res+=(k/loop[x]-1)*(cnt-loopst+1);
                k=loop[x];
            }
            else res+=k/loop[x]*(cnt-loopst+1), k%=loop[x];
            k+=col[loopst-1][x];
            res+=find_(x, k);
            cout<<res-1<<"\n";
        }
    }
    return 0;
}

你可能感兴趣的:(2020 CCPC Wannafly Winter Camp Day7 部分题解(AFGHJKL))