2021.ccpc 网络赛

  1. Cut The Wire

签到题,不难,就是题意有点难懂,注意奇偶分情况考虑

  1. Time-division Multiplexing 阴间阅读理解

大意:阅读理解题,题意巨难懂。大致意思就是给定 n 个长度不超多 12 的字符串,n<=1000,先将字符串按以下方式进行排列,即每个字符串有一个指针,刚开始指向每个字符串的第一个字符,然后从第一个字符串开始一直到最后一个字符串,将指针对应的字符依次拿出,然后指针向后移动,当指针在对应字符串的最后一个字母位置时继续移动回到第一个字符,依次往复构成新的字符串,求新字符串中包含n个字符串所有字母的最短子串的长度。例如:abc和bd 构成的新字符串为 abbdcbad…包含所有字母的最短子串为 cbad 长度为4.

思路:通过两个字符构造我们会发现,构建出的新的字符串的是有循环节的,即所有字符串长度的最小公倍数。12以内的最大的最小公倍数也不会超过1e5。单个循环节的长度为 lcm*n,两个循环节的接头处也可能构造出,所有我们实际要搞两个循环节。求最短的长度用双指针就好了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=1010,inf=0x3f3f3f3f;
char str[N][20];
int n,stl[N],mp[30],cnt[30];
int gcd(int a,int b)
{
    return b ? gcd(b,a%b):a;
}
int lcm(int a,int b)
{
    return a*b/gcd(a,b);
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)
    {
        memset(cnt,0,sizeof cnt);
        memset(mp,0,sizeof mp);
        cin>>n;
        for(int i=0;i<n;i++)cin>>str[i],stl[i]=strlen(str[i]);

        for(int i=0;i<n;i++)
            for(int j=0;j<stl[i];j++)
                mp[str[i][j]-'a']++;

        int m=0;
        for(int i=0;i<26;i++)if(mp[i])m++;

        int len=1;
        for(int i=0;i<n;i++)len=lcm(len,stl[i]);

        string s="";
        for(int i=0;i<len;i++)
        {
            for(int j=0;j<n;j++)
                s+=str[j][i%stl[j]];
        }
        s+=s;
        int ans=inf,ct=0;
        for(int i=0,j=0;i<s.size();i++)
        {
            cnt[s[i]-'a']++;
            if(cnt[s[i]-'a']==1)
            {
                ct++;
                if(ct==m)
                {
                    ans=min(ans,i-j+1);
                    while(1)
                    {
                        cnt[s[j]-'a']--;
                        if(!cnt[s[j]-'a'])
                        {
                            j++,ct--;
                            break;
                        }
                        else j++,ans=min(ans,i-j+1);
                    }
                }
            }
        }
        cout<<ans<<"\n";
    }
    return 0;
}
  1. Power Sum 思维,性质

大意:给定一个整数 n ,现有个长度为 k 序列 a , a i a_i ai=1或 -1。(1<=k<=n+2)。 求 满足 ∑ a i × i 2 ∑a_i×i^2 ai×i2=n ( 1 < = i < = k ) (1<=i<=k) (1<=i<=k)的序列 a。

思路:首先对于完全平方数是有一定性质。 x 2 − ( x + 1 ) 2 − ( x + 2 ) 2 + ( x + 3 ) 2 = 4 x^2-(x+1)^2-(x+2)^2+(x+3)^2=4 x2(x+1)2(x+2)2+(x+3)2=4 任意相邻的四个数,一旦它们的符号是 - - + +

那么它们的和就是固定的。因为 k 和 n 相差不多,而且我们正好每四个数的贡献是四,平均下来每个数的贡献都是1,一定能保证构造出,这也就是我们要这样构造的原因。我们只需要用前面几个数构造 n%4的余数,必然不会超过四个数,构造出1,2,3。

代码如下:

#include <bits/stdc++.h>
using namespace std;
int n;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _;
    cin>>_;
    while(_--)
    {
        string s="";
        cin>>n;
        if(n%4==1)s+='1';
        if(n%4==2)s+="0001";
        if(n%4==3)s+="01";
        n/=4;
        for(int i=0;i<n;i++)s+="1001";
        cout<<s.size()<<"\n";
        cout<<s<<"\n";
    }
    return 0;
}
  1. Function 思维,预处理,数学

大意:定义一个g(x)函数,代表 x 各个数位上数字之和,例如 g(123)=1+2+3。另外有个函数 f ( x ) = A x 2 g ( x ) + B x 2 + C x g 2 ( x ) + D x g ( x ) f(x)=Ax^2g(x)+Bx^2+Cxg^2(x)+Dxg(x) f(x)=Ax2g(x)+Bx2+Cxg2(x)+Dxg(x) 1<=x<=N ,求 f(x)的最小值 0 ≤ ∣ A ∣ ≤ 1 0 3 , 0 ≤ ∣ B ∣ , ∣ C ∣ , ∣ D ∣ ≤ 1 0 6 , 1 ≤ N ≤ 1 0 6 0≤|A|≤10^3,0≤|B|,|C|,|D|≤10^6,1≤N≤10^6 0A103,0B,C,D106,1N106

思路: x最大是 1e6 也就是说 g(x)最大也就是g(999999)=54。也就是说g(x)的变化范围很小,我们可以固定g(x) 进而求出最值。固定 g(x)=i之后我们就可以看做二次函数求最小值了。直接利用二次函数求最小值解得的x不一定满足 g(x)=i,我们只需要找到距离他最近的g(x)=i就好了。直接暴力查找肯定是不行的,但是 g(x)=i 的 x 是固定的,对于固定的部分我们可以考虑预处理,之后我们就可以二分查找了。对于二次项系数 <=0 的,最小值肯定在区间端点处产生。对于 >0 的,如果我们用对称轴+二分去找点的话会有很多比较麻烦的点不在区间内的问题,所以我们直接三分求极值。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N=1000010;
typedef long long LL;
vector<int> p[55];
int cale(int x)
{
    int s=0;
    while(x)
    {
        s+=x%10;
        x/=10;
    }
    return s;
}
LL a,b,c,d,n;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    for(int i=1;i<=54;i++)p[i].push_back(-1e9);
    for(int i=1;i<=1e6;i++)p[cale(i)].push_back(i);
    int _;
    cin>>_;
    while(_--)
    {
        cin>>a>>b>>c>>d>>n;
        LL ans=1e18;
        for(int i=1;i<=54;i++)
        {
            LL A=a*i+b,B=c*i*i+d*i;
            if(p[i][1]>n)continue;
            int l=1,r=upper_bound(p[i].begin(),p[i].end(),n)-p[i].begin()-1;// 区间从 1 开始,若从 0 开始则下面改为 while(l<r)
            ans=min(ans,A*p[i][l]*p[i][l]+B*p[i][l]);
            ans=min(ans,A*p[i][r]*p[i][r]+B*p[i][r]);
            if(A>0)
            {
                while(l<=r)
                {
                    int lmid=l+(r-l)/3,rmid=r-(r-l)/3;
                    LL fl=A*p[i][lmid]*p[i][lmid]+B*p[i][lmid];
                    LL fr=A*p[i][rmid]*p[i][rmid]+B*p[i][rmid];
                    if(fl<=fr)r=rmid-1;
                    else l=lmid+1;
                }
                ans=min(ans,A*p[i][l]*p[i][l]+B*p[i][l]);// 因为是 fl<=fr,最终极小值在 l 处 
            }
        }
        cout<<ans<<"\n";
    }  
    return 0;
}
  1. Command Sequence 思维、签到

思路:类似于括号匹配,转换成前缀和就好了。

  1. K题 Shooting Bricks

大意:有n * m 的矩阵,每个格子对应一个砖块,从最后一行开始打砖块,一发子弹打破一个砖块。对于位于 (i,j) 的砖块,只有当 (i+1,j) 的砖块已经被打破,该砖块才能被打,打破一个砖块会消耗一枚子弹,但对于某些砖块,打破之后可以额外获得一发子弹,打破砖块可以获得对应的分数。问有 k 发子弹,求获得分数的最大值。

思路:很容易想到一个看似正确的做法,即对于每一类将其分成 n 组,对于可以获得子弹的砖块相当于不消耗子弹,预处理出每一组的体积和价值,然后就是一个简单的分组背包。看似没问题,但是仔细想想,我们这样直接分组是不对的,直接看个反例,

即只有一发子弹,砖块情况如下:

Y

Y

N

当我们把三个砖块看成一组时,我们预处理的体积是 1 ,但实际上这样是不行的,因为能获得子弹的前提是你得先有子弹去打

但对于这样的情况:

N

Y

Y

又是可以打的。

所以 N Y 顺序不同是不一样的,不能简单的合在一起。

所以我们状态定义的时候得多加一维状态将他们区分开:

f[j,k,0/1] 表示在前 i 组 中,子弹数不超过 k 时,且最后一发子弹是否打在了第 j 列,0 代表在,1代表不在

从下往上看,我们预处理分组的时候都是以某一行为结尾的前缀,因为我们要关注最后一颗子弹,所以我们预处理的时候也得处理处两个状态,即是否为最后一颗子弹打的,0/1代表是与否。并且对于最后一颗子弹打的位置一定是 N , 因为打了 N 后一旦能打 Y 我们必定去打。也就是 N 的地方我们既可以划分到0中,有可以划分到 1中,而对于 Y 只划分到 1 中。

状态转移分以下几种情况:

(1)该列的砖块不打,直接跳过

(2)最后一颗子弹不在 1-j 列中,那么必然不在 1-j-1 列中,即从 f[j-1,k,1]转移过来

(3)最后一颗子弹在 1-j 列中,然后又可以细分成两个集合,即在第j列和不在第j列,取max。

另外和分组背包一样可以去掉一维

具体看代码注释:

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=210;
int n,m,V;
int a[N][N],b[N][N],w[N][N][2],f[N][N][2];
char ch;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int _=1;
    //cin>>_;
    while(_--)
    {
        memset(f,0,sizeof f);
        memset(w,0,sizeof w);
        cin>>n>>m>>V;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
            {
                cin>>a[i][j]>>ch;
                if(ch=='Y')b[i][j]=1;
                else b[i][j]=0;
            }
        for(int i=1;i<=m;i++)
        {
            int cnt=0;// 根据消耗子弹数分组
            for(int j=n;j>=1;j--)
            {
                if(b[j][i])w[i][cnt][1]+=a[j][i];
                else cnt++,w[i][cnt][1]=w[i][cnt-1][1]+a[j][i],w[i][cnt][0]=w[i][cnt-1][1]+a[j][i];
            }
        }
        for(int i=1;i<=m;i++)
            for(int j=0;j<=V;j++)
            {
                f[i][j][1]=f[i-1][j][1];//继承上一轮的状态
                f[i][j][0]=f[i-1][j][0];
                for(int k=0;k<=min(n,j);k++)
                {
                    f[i][j][1]=max(f[i][j][1],f[i-1][j-k][1]+w[i][k][1]);//最后一颗子弹不在1~i列,则必然也不在1~i-1列
                    // 若最后一颗子弹在 1~i 列,分两种情况
                    // 第一种情况,在第 i 列,显然打第 i 列是时子弹数不为0,并且从步不在 1~i-1列的状态转移过来
                    if(k)f[i][j][0]=max(f[i][j][0],f[i-1][j-k][1]+w[i][k][0]);
                    // 第二种情况,在 1~i-1 列,那么也在1~i列中,并且打1~i-1 的子弹不能为0
                    if(j>k)f[i][j][0]=max(f[i][j][0],f[i-1][j-k][0]+w[i][k][1]);
                }
            }
        cout<<f[m][V][0]<<"\n"; // 最终结束没有砖块打了,则相当于必有最后一颗子弹落在了某一列 
    }
    return 0;
}
  1. Remove 数学、思维

大意:给定 一个整数 n , 以及含有 P 个质数的质数集 。对于一堆石子数为 x 的石子,每次可以在质数集选择一个质数p,取走x%p 个石子。x变成 x-x%p。 质数可以重复使用,记 f(x) 为将 x 个石子变为 0 的最小操作步数。求对于每个 i (1<=i<=n)的 f(i)。 若 i 个石子没办法变为 0 则f(i)=0。1<=n<=1e6,质数集中的质数不超过 1e5 个。

思路:我们只处理能通过若干次操作将个数变为0的石子数目,我们记f(i)是将i个石子变为0的最少操作次数。首先我们可以感性的f(i) 是单调不降的,且 0 < = f ( i + 1 ) − f ( i ) < = 1 0<=f(i+1)-f(i)<=1 0<=f(i+1)f(i)<=1。所以如果我们知道了对于一个 i 后面多少个数是等于 f(i)+1的话,我们就可以线性递推了。记 ex(i) 代表 i 之后有多少个数等于 f(i)+1,即 f ( k ) = f ( i ) + 1 , k < i + e x ( i ) f(k)=f(i)+1,k<i+ex(i) f(k)=f(i)+1,k<i+ex(i)。接下来我们就是怎么去求 ex(i)。 显然对于质数集中出现的质数ex(p[i])=p[i]。很容易理解,假设 p[i] 最少需要 y 次操作,那么p[i]+1~p[i]+p-1都可以先通过选择 p[i] 操作一次变成 p[i]。对于任意一个数 j,假设 j 可以通过一次操作变成 i,即 f(j)=f(i)+1。即 j 通过某个质数 p 变成 i , 也就是说对于 j 来数, i+1<=j <=i+p-1 都是满足 f(j)=f(i)+1 的。即 ex(i)=p。又因为 j 是通过 p 变成 i 的,p必然是 i 的质因子,为了使 ex数组尽可能大,显然我们选择最大质因子。对于最大质因子我们不好处理,所以我们欧拉筛求出每个数的最小质因子minp。minp和i/minp 一定是包含了最大质因子的。

更新 f(i)的时候直接双指针就好了。

具体看代码:

#include <bits/stdc++.h>
using namespace std;
const int N=2000010;
typedef long long LL;
typedef unsigned long long ULL;
int n,m,p[N],f[N],mp[N],ex[N];
bool st[N];
ULL q[N];
void init()
{
    int cnt=0;
    mp[1]=1;
    for(int i=2;i<N;i++)
    {
        if(!st[i])
        {
            p[cnt++]=i;
            mp[i]=i;
        }
        for(int j=0;j<cnt;j++)
        {
            if(i*p[j]>N)break;
            st[i*p[j]]=1;
            mp[i*p[j]]=p[j];
            if(i%p[j]==0)break;
        }
    }
    q[0]=1;
    for(int i=1;i<=N-10;i++)q[i]=q[i-1]*23333;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    init();
    int _;
    cin>>_;
    while(_--)
    {
        cin>>n>>m;
        int mx=0;
        for(int i=0;i<m;i++)
        {
            int x;
            cin>>x;
            mx=max(x,mx);
            ex[x]=x;
        }
        f[1]=0;
        ex[0]=mx;
        for(int i=1,j=0;i<=n;++i)
        {
	        while(i-j>=ex[j])
	        {
	            ++j;
	            if(i==j) break;
	        }
	        if(i==j)break; //一旦 i==j 则证明能变成0的到头了,并且f[j]已经算过了,不能再多加了
        	f[i]=f[j]+1;
        	ex[i]=max(ex[mp[i]],ex[i/mp[i]]); //如果 i 本身就是质数,相当于可以 把 1 看成质因子,不影响结果,对于其他的总能用最大质因子去更新
    	}
 
    	ULL ans=0;
    	for(int i=1;i<=n;i++)ans+=f[i]*q[n-i];
    	cout<<ans<<"\n";


    	for(int i=1;i<=n;i++)f[i]=0;
    	for(int i=1;i<=mx;i++)ex[i]=0;
       	
    }
    return 0;
}

总结:找到石子数目变化的实质,找到递推关系就好了。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值