Codeforce 8.1-8.14做题记录

Magical Array(GOOD)

给定数组b长度为m。现有n个数组 c i , 1 ≤ i ≤ n c_i,1 \leq i \leq n ci,1in,初始都与数组b相同。从中选择一个数组 c k c_k ck。每次可以进行如下操作:

  • 对任何一个数组 c i , i ≠ k c_i,i \neq k ci,i=k,选择下标p,q,p<q,使得 c i [ p ] , c i [ q ] c_i[p],c_i[q] ci[p],ci[q]减1。使得 c i [ p − 1 ] , c i [ q + 1 ] c_i[p-1],c_i[q+1] ci[p1],ci[q+1]加1。
  • 对数组 c k c_k ck,选择下标p,q,p<q,使得 c i [ p ] , c i [ q ] c_i[p],c_i[q] ci[p],ci[q]减1。使得 c i [ p − 1 ] , c i [ q + 2 ] c_i[p-1],c_i[q+2] ci[p1],ci[q+2]加1。

在经过上述操作若干次后给定数组 c 1 , c 2 ⋯ c n c_1,c_2\cdots c_n c1,c2cn要求找出k,并且求出在其上使用的第二种操作的次数。
思路
这道题需要我们从整体上把握。
物理角度
第一种操作是非常对称的,第二种操作是非常不对称的。如果将 c i c_i ci中每个元素都看作是有质量的物体分别放在数轴上下标i的位置。从物理的角度来看第一种操作好像是在四个比较对称的位置加减质量一样的,直观来看质心不会变化,而第二种操作质心会变化。
编码角度
从整体上把握找出一种“编码方法”使得那个特定的k的编码和其他c数组的编码不同。第一种操作涉及下标为p,q,p-1,q+1,第二种操作涉及下标为p,q,p-1,q+2导致不同所以编码时需要可以涉及这个下标信息。对第一种操作如果将下标与数本身相乘则变化前后涉及下标的项以及常数项会相互抵消,就不会变化。

上述策略指向我们对数组的编码方式是
h a s h ( c i ) = ∑ k c i [ k ] ⋅ k hash(c_i) = \sum _k c_i[k] \cdot k hash(ci)=kci[k]k
对第一种操作,只考虑变化的四个下标p,q,p-1,q+1下标处变化后为:
( c i [ p ] − 1 ) ⋅ p + ( c i [ q ] − 1 ) ⋅ q + ( c i [ p − 1 ] + 1 ) ⋅ ( p − 1 ) + ( c i [ q + 1 ] + 1 ) ⋅ ( q + 1 ) = c i [ p ] ⋅ p + c i [ q ] ⋅ q + c i [ p − 1 ] ⋅ ( p − 1 ) + c i [ q + 1 ] ⋅ ( q + 1 ) (c_i[p] - 1) \cdot p + (c_i[q] - 1) \cdot q + (c_i[p-1] + 1) \cdot (p- 1) + (c_i[q+1] +1) \cdot (q + 1) = c_i[p] \cdot p + c_i[q] \cdot q + c_i[p-1] \cdot (p-1) + c_i[q+1] \cdot (q+1) (ci[p]1)p+(ci[q]1)q+(ci[p1]+1)(p1)+(ci[q+1]+1)(q+1)=ci[p]p+ci[q]q+ci[p1](p1)+ci[q+1](q+1)
从物理角度说质心不变化。
对第二种操作同理,同理可以发现每操作一次质心向右移动一个单位。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;


int main()
{
    int t;
    scanf("%d",&t);
    for(int i = 0;i<t;i++)
    {
        int n,m;
        scanf("%d%d",&n,&m);
        vector<ll> m_;
        
        for(int j = 0;j<n;j++)
        {
            ll a = 0;
            for(int k = 0;k<m;k++)
            {   
                ll c; 
                scanf("%lld",&c);
                a += c * (ll)k;
            }
            m_.push_back(a);
        }
        for(int j = 0;j<n;j++)
        {
            if(j == 0 && m_[0]!=m_[1] && m_[j + 1] == m_[j + 2])
            {printf("%d %lld\n",1,m_[0]-m_[1]);break;}
            else if(j == n - 1 || j!=0 && j < n - 1 && m_[j-1]==m_[j+1] && m_[j] != m_[j-1])
            {
                printf("%d %lld\n",j+1,m_[j]-m_[j-1]);break;
            }
        }
    }
    system("pause");
    return 0;
}

总结:可以找前缀和,差分,奇偶性(见下一篇笔记中的一道题)等。

Chip Move(前缀和优化dp)

题意:一维数轴上欲从点0移动到点 n , n^, n,(只能向右移动)。第i次移动距离必须是(k+i-1)的倍数(不等于0)。问当 n , n^, n,从1取值到n时问移动种类数分别是多少。
数据范围:
在这里插入图片描述
题解
设移动的次数设为h,则h最大值满足 ( h + 1 ) h ≤ n (h+1)h \leq n (h+1)hn,则 h h h O ( n ) O(\sqrt n) O(n )级别的。因此设 d p [ i ] [ j ] dp[i][j] dp[i][j]为使用i次移动移动到点j的方案数。

则总状态数量为 O ( n n ) O ( n \sqrt n) O(nn )的。
状态转移方程:
d p [ i ] [ j ] = d p [ i − 1 ] [ j − ( k + i − 1 ) ] + d p [ i − 1 ] [ j − 2 ( k + i − 1 ) ] + ⋯ dp[i][j] = dp[i - 1][j - (k + i - 1)] + dp[i-1][j - 2(k+i - 1)] + \cdots dp[i][j]=dp[i1][j(k+i1)]+dp[i1][j2(k+i1)]+
只要 j − q ⋅ ( k + i − 1 ) ≥ 0 j - q \cdot (k + i - 1) \geq 0 jq(k+i1)0
复杂度不可接收
但可以将上述和维护成一个前缀和。
初始时:
p r e [ i − 1 ] [ j ] = d p [ i − 1 ] [ j ] pre[i - 1][j] = dp[i - 1][j] pre[i1][j]=dp[i1][j]
从小到大遍历j每一次只需:
p r e [ i − 1 ] [ j ] + = p r e [ i − 1 ] [ j − ( k + i − 1 ) ] pre[i - 1][j] += pre[i - 1][j - (k + i - 1)] pre[i1][j]+=pre[i1][j(k+i1)]
这样就有:
d p [ i ] [ j ] = p r e [ i − 1 ] [ j ] dp[i][j] = pre[i-1][j] dp[i][j]=pre[i1][j]
注意上述转移是i-1到i的,因此可以利用滚动数组优化。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;



int main()
{
    ll n,k;
    scanf("%lld%lld",&n,&k);
    ll pre[n+1],dp[n+1],ans[n+1];
    memset(dp,0,(n+1)*sizeof(ll));
    memset(ans,0,(n+1)*sizeof(ll));
    dp[0] = 1;
    for(ll i = k;(k + i) * (i - k + 1) / 2<= n;i++)
    {
        //更新前缀和
        for(int j  = 0;j<=n;j++){pre[j]=dp[j]; if(j- i >= 0) pre[j] = (pre[j] + pre[j - i]) % mod;}
        //利用前缀和更新dp值
        for(int j = 0;j<=n;j++) {if(j - i >= 0) dp[j] = pre[j-i]; else dp[j] = 0;}
        for(int j = 1;j<=n;j++) ans[j] = (ans[j] + dp[j]) % mod;
    }  
    for(int i  =1;i<n;i++) printf("%d ",(int)ans[i]);
    printf("%d",(int)ans[n]);
    system("pause");
    return 0;
}

Robot in a Hallway(very very good)

题意:给定2行m列的矩阵每个格点有一个数字。机器人初始时在格点(0,0)处。每个格点(i,j)上有一个数字a[i][j],表明在a[i][j]后该格点会解锁。格点只有在解锁后机器人才能进入(即机器人可能最早进入(i,j)的时间为a[i][j]+1)。每单位时间机器人可以上下左右移动到相邻的格点。求机器人在不访问同一格点两次及以上的情况下,机器人访问完所有格点所需最少时间。
数据范围: m ≤ 2 e 5 m \leq 2e^5 m2e5
题解:
我认为这道题非常好,结合了许多知识点又有一定的思维量,将这道题严谨的过程写一下。
首先,由不访问一个结点两次及以上,只可能是如下的访问路径:
全直走:
在这里插入图片描述

部分Z字形走+剩下直走(由从上结点开始和从下结点开始共2种情形)
在这里插入图片描述在这里插入图片描述

全Z字形走:
在这里插入图片描述
其中全直走,全Z字形走的情形根据递推关系 d p = m a x ( d p + 1 , a [ i ] [ j ] + 1 ) dp = max(dp+1,a[i][j] + 1) dp=max(dp+1,a[i][j]+1)关系即可求出。关键在于部分Z字形的情况,而部分Z字形的Z字形也可以根据上述递推关系求解。于是问题变为如何快速求解部分Z字形后直走的部分所用时间,当然也可以根据递推关系求解但是不断求解显然时间复杂度不可接收,能否找到一些增量关系呢?
考察一下递推关系 d p = m a x ( d p + 1 , a [ i ] [ j ] + 1 ) dp = max(dp+1,a[i][j] + 1) dp=max(dp+1,a[i][j]+1),最开始dp值就是将部分Z字形路径走完后的时间。
假设我们递推n次。在这n次递推中,我们可能每一次都是令当前的dp加1,也有可能是直接取 a [ i ] [ j ] + 1 a[i][j] + 1 a[i][j]+1这个值。关键在于一旦取了 a [ i ] [ j ] + 1 a[i][j]+1 a[i][j]+1这个值后,前面的所有路径都无关了。因此如果假设某个 a [ i ] [ j ] + 1 a[i][j]+1 a[i][j]+1在递推过程中在max函数中被取到我们可以仅仅考察最后一次取到的对应的 a [ i , ] [ j , ] a[i^,][j^,] a[i,][j,],这意味着如果在其前面有a[i][j]+1在max函数中被取到则加上差值后仍然小于 a [ i , ] [ j , ] + 1 a[i^,][j^,]+1 a[i,][j,]+1,对 a [ i , ] [ j , ] a[i^,][j^,] a[i,][j,]后面所有点意味着从其出发加上时间差值大于等于后面所有点的解锁时间。这引导我们将时间差信息融合到a[i][j]本身。因此如果假设 a [ i , ] [ j , ] + 1 a[i^,][j^,]+1 a[i,][j,]+1在第q次递推取到,显然其 a [ i , ] [ j , ] + ( n − q ) a[i^,][j^,] + (n - q) a[i,][j,]+(nq)具有最大值。反之若该值最大则考察一下差值则也容易证明其是最后一次被取到。
于是递推式的最终结果就是 m a x ( d p 0 + n − 1 , a [ i , ] [ j , ] + ( n − q ) ) max(dp_0 + n - 1,a[i^,][j^,] + (n - q)) max(dp0+n1,a[i,][j,]+(nq))
推广到本题,就是维护后缀每个 a [ i ] [ j ] + j a[i][j]+j a[i][j]+j a [ i ] [ j ] − j a[i][j]-j a[i][j]j的最大值。我这里使用栈来实现。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;



int main()
{
    int t; cin>>t;
    for(int  i = 0;i<t;i++)
    {
        int m;scanf("%d",&m);
        ll a[2][m];
        for(int p = 0;p<2;p++) for(int q = 0;q<m;q++) scanf("%lld",&a[p][q]);
        stack<pair<ll,int>> sufmmax1,sufmmax2,sufpmax1,sufpmax2;
        sufmmax1.push({a[0][m-1]-(m-1),m-1}); sufmmax2.push({a[1][m-1]-(m-1),m-1});
        sufpmax1.push({a[0][m-1]+(m-1),m-1}); sufpmax2.push({a[1][m-1]+m-1,m-1});
        for(int q = m - 2;q>=0;q--)
        {
            if(a[0][q] - q > sufmmax1.top().first) sufmmax1.push({a[0][q]-q,q});
            if(a[1][q] - q > sufmmax2.top().first) sufmmax2.push({a[1][q]-q,q});
            if(a[0][q] + q > sufpmax1.top().first) sufpmax1.push({a[0][q]+q,q});
            if(a[1][q] + q > sufpmax2.top().first) sufpmax2.push({a[1][q]+q,q});
        }
        ll ans;
        ll curtime = 0;
        for(int q = 1;q<m;q++) curtime = max(curtime + 1,a[0][q] + 1);
        curtime = max(curtime + 1,a[1][m- 1] + 1);
        for(int q = m  - 2;q>=0;q--) curtime = max(curtime + 1,a[1][q] + 1);
        ans = curtime;
        ll i_ = 0,j_ = 0;
        //z字形走位
        curtime = 0;
        while(j_ <= m - 1)
        {
            if(sufmmax1.top().second == j_) sufmmax1.pop();
            if(sufmmax2.top().second == j_) sufmmax2.pop();
            if(sufpmax1.top().second == j_) sufpmax1.pop();
            if(sufpmax2.top().second == j_) sufpmax2.pop();
            i_ = i_ ^ 1;
            curtime  = max(curtime + 1,a[i_][j_] + 1);
            if(m - j_ -1 >= 2) ans = min(ans,max(curtime +2*m - 2*j_ - 2,max(2*m - j_ - 1 + sufmmax2.top().first,-j_ + sufpmax1.top().first)));
            j_++;
            if(j_ > m - 1) break;
            curtime = max(curtime + 1,a[i_][j_] + 1);
            if(sufmmax1.top().second == j_) sufmmax1.pop();
            if(sufmmax2.top().second == j_) sufmmax2.pop();
            if(sufpmax1.top().second == j_) sufpmax1.pop();
            if(sufpmax2.top().second == j_) sufpmax2.pop();
            i_ = i_ ^ 1;
            curtime = max(curtime + 1,a[i_][j_] + 1);
            if(m - j_ - 1 >= 2) ans = min(ans,max(curtime + 2*m - 2*j_ -2,max(-j_ + sufpmax2.top().first,2*m - j_ - 1 + sufmmax1.top().first)));
            j_++;
            if(j_ <= m - 1) curtime = max(curtime + 1,a[i_][j_] + 1);
        }
        ans = min(ans,curtime);
        printf("%lld\n",ans);
        
    }
    system("pause");
    return 0;
}

Tournament Countdown(交互1800)

题意:编号1-n的人两两进行单淘汰赛。即1号2号比赛,胜者进入下一轮,3号4号比赛,胜者进入下一轮,以此类推,如图所示。
在这里插入图片描述
每次可以查询任意两个编号人物获胜次数大小比较情况(给出大于,小于,等于三种回复)。要求查询次数不超
的前提下求出最终胜者。
在这里插入图片描述

题解:查询次数很像等比数列和。即
2 + 2 ⋅ 4 + 2 ⋅ 4 2 + ⋯ 2 ⋅ 4 n 2 − 1 = 2 n + 1 − 2 3 2 + 2 \cdot 4 + 2\cdot 4^2 + \cdots 2 \cdot 4^{\frac{n}{2} - 1} = \frac{2^{n+1} - 2}{3} 2+24+242+242n1=32n+12
因此考虑每4个一组求出答案。假设现在只剩4个对比1号3号大小后分类讨论即可。注意这道题隐含条件是对这4个人有2个最小的相等次数(第一轮被淘汰的两个人)以及2个较大的次数且这两个较大次数一定是不相等的。2次查询就可以获取最终的胜者。递归应用上述结论即可。
ps:另一种解读 ⌈ 1 3 ⋅ 2 n + 1 ⌉ \lceil \frac{1}{3} \cdot 2^{n+1} \rceil 312n+1的方法是要求每2个query可以划去3个数字。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;



int main()
{
    int t;
    scanf("%d",&t);
    for(int i  = 0;i<t;i++)
    {
        int n; scanf("%d",&n);
        fflush(stdin); fflush(stdout);
        vector<int> a;
        for(int k = 1;k<=(1 << n);k++) a.push_back(k);
        while(a.size() >= 4)
        {
            vector<int> b;
            for(int k = 0;k<=a.size() - 4;k+=4)
            {
                int q = a[k],w = a[k+1],e=a[k+2],r=a[k+3];
                printf("? %d %d\n",q,e);
                fflush(stdout);
                fflush(stdin);
                int o; scanf("%d",&o);
                fflush(stdout); fflush(stdin);
                if(o == 0)
                {
                    printf("? %d %d\n",w,r);fflush(stdout); fflush(stdin);
                    int o; scanf("%d",&o);
                    b.push_back(o == 1?w:r);
                }
                else if(o == 1)
                {
                    printf("? %d %d\n",q,r);fflush(stdout); fflush(stdin);
                    int o; scanf("%d",&o);
                    b.push_back(o == 1?q:r);
                }
                else
                {
                    printf("? %d %d\n",w,e);fflush(stdout); fflush(stdin);
                    int o; scanf("%d",&o);
                    b.push_back(o == 1?w:e);
                }
            }
            a.clear();
            for(int v:b) a.push_back(v); 
        }
        fflush(stdout);
        fflush(stdin);
        printf("? %d %d\n",a[0],a[1]);
        fflush(stdout);
        fflush(stdin);
        int o; scanf("%d",&o);
        if(o == 1) printf("! %d\n",a[0]); else printf("! %d\n",a[1]);
        fflush(stdout);
        fflush(stdin);
    }
    system("pause");
}

重要结论:
在这里插入图片描述

Tokitsukaze and Two Colorful Tapes(1900)

题意:2行网格,每行不同网格颜色不同且都有n种不同颜色。要求为每种颜色绑定一个不同的数字(1-n之间),使得同一列网格上数字之差绝对值的和(对所有列求和)最大。
在这里插入图片描述

数据范围:n在1e5规模
题解:同一颜色为图中一个结点,边为连接同一列从上一行到下一行颜色结点。则该图由若干不相交的环构成。对同一环若取值集合确定,欲为环上结点赋值,则贪心地将最小值和最大值相匹配(严格证明稍后补充)。则对环长为k的环而言,其所能达到的最大值就是最大的 ⌊ k / 2 ⌋ \lfloor k/2 \rfloor k/2个数的2倍减去最小的 ⌊ k / 2 ⌋ \lfloor k/2 \rfloor k/2个数的2倍(环长奇数时中间的数挖去了)。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;



int main()
{
    int e;
    scanf("%d",&e);
    for(int i = 0 ;i<e;i++)
    {
        int n; scanf("%d",&n);
        int adj[n+1];
        int a[n];
        for(int k = 0;k<n;k++) scanf("%d",&a[k]);
        for(int k = 0;k<n;k++) {int v;scanf("%d",&v);adj[a[k]]=v;}
        bool vis[n+1]; memset(vis,0,(n+1)*sizeof(bool));
        int t = 0;
        for(int k = 1;k<=n;k++)
        {
            if(!vis[k])
            {
                int count = 0;
                int cur = k;
                do{count++;vis[cur]=true;cur=adj[cur];}while(cur!=k);
                t+=count/2;
            }
        }
        ll t_=ll(t); cout<<2*(ll)n*t_-2*t_*t_<<endl;
    }
    system("pause");
    return 0;
}

Count the Trains(2000)

题意:给定数组a,每次操作可以将其中一个数字减d。求每次操作后前缀最小值数组中不同元素个数。
ps:前缀最小值 b [ i ] = m i n ( a [ 0 ] , a [ 1 ] , ⋯ a [ i ] ) b[i] = min(a[0],a[1],\cdots a[i]) b[i]=min(a[0],a[1],a[i])
在这里插入图片描述

题解:利用set容易维护每个最小值突变点的下标。
关键在于复杂度分析,不会存在很多次多个段需要合并的情况
摊还下来删除的数量是线性的

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const ll mod = 998244353;
     
     
    //q
    int main()
    {
        int e;
        scanf("%d",&e);
        for(int i = 0 ;i<e;i++)
        {
           
            int n,m; scanf("%d%d",&n,&m);
            int a[n];
            for(int k = 0;k<n;k++) scanf("%d",&a[k]);
            vector<int> ans;
            set<int> o;  o.insert(0); auto  iter=o.begin();
            for(int k = 1;k<n;k++)
            {
                if(a[k]<a[*iter]) {o.insert(k);iter++;}
            }
            int l = o.size();
            for(int q = 0;q<m;q++)
            {
                int k,d; scanf("%d%d",&k,&d);
                k--;
                auto iter = o.upper_bound(k);iter--;
                //cout << *iter<<endl;
                if(a[k]-d<a[*iter])
                {
                    o.insert(k);int temp = a[k]-d;
                    //cout<<*iter<<endl;
                    if(k!=*iter)iter++;iter++;//
                    //cout<<*iter<<endl;
                    while(iter!=o.end() && a[*iter]>=temp) 
                    {iter = o.erase(iter);}//cout<<*iter<<endl;cout << o.size() << endl;}
                }
                a[k]-=d;
                ans.push_back(o.size());
                //cout << o.size() << endl;
            }
            for(int q = 0;q<ans.size();q++) printf("%d ",ans[q]);
            printf("\n");
            //fflush(stdin);fflush(stdout);
        }
        system("pause");
        return 0;
    }

需要注意的点:应使用o.upper_bound而不应该使用upper_bound(o.begin,o.end,k)否则超时。当时调了很久。。
在这里插入图片描述
两者的区别参照https://en.cppreference.com/w/cpp/algorithm/lower_bound

Manipulating History(1700 GOOD)

题意:给定单个字符组成的字符串(初始字符串)。每次操作中,选取长度不为0的字串替换为另一字串。现给定每次操作中被替换的字串和替换成的目标字串(但是顺序全部打乱一共2n个即一共进行n次操作),以及最终形成的字符串。
求这个初始字符串。
数据范围: n ≤ 1 e 5 n\leq 1e^5 n1e5
题解整体考虑,不要关注局部。为何初始字符串仅含1个字符,他有什么与众不同的性质?操作过程中,一个不是初始字符的任意字符如果产生且不存在于最终字符串则显然偶数个序列包含他(消失和产生配对),如果存在于最终字符串则显然奇数个序列包含他,对于初始字符类似。则当全部操作执行完毕后,不是初始字符的字符当且仅当在所有输入(包括最终字符串)中出现偶数次,是初始字符的字符当且仅当在所有输入中出现奇数次。注意上述过程我们并没有显式地恢复操作序列。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;





int main()
{
    int t;
    scanf("%d",&t);
    for(int i  =0;i<t;i++)
    {
        int n; cin>>n;
        int a[26];memset(a,0,sizeof(a));
        for(int k = 0;k<2*n+1;k++){string t;cin>>t;for(char ch:t)a[ch-'a']++;}
        for(int k = 0;k<26;k++){if(a[k]%2==1){cout<<(char)('a'+k)<<endl;break;}}
    }
    system("pause");
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值