10月12日考试 题解(数位DP+递归+二分图+背包)

题目难度与题目顺序没有一点关系……

T1 num

原题:CF55D

题目大意:求$[l,r]$内有多少数满足各数位上的数都能整除原数。$r\leq 10^{18}$

显然数位DP。可以发现,如果几个数的最小公倍数能整除原数,那么这几个数也一定能够整除原数。所以我们不妨从$1$到$9$的最小公倍数入手。因为题目要求的是最后看这个数能不能满足要求,所以我们在记搜时只用考虑在模$1$到$9$的最小公倍数意义下的值就可以了。同时还要记录一个当前的最小公倍数;发现$20\times 2520\times 2520$开不下,不妨离散化$2520$的因数(不超过$50$个)。于是这道题被我们解决了。

时间复杂度$O(metaphisics)$

代码:

#include<cstdio>
#include<iostream>
#include<cstring>
#define int long long
using namespace std;
const int p=2520;
int f[20][2551][55],a[20],num[2551],T,l,r,len,cnt;
inline int gcd(int x,int y){
    return (y==0)?x:gcd(y,x%y);
}
inline int LCM(int x,int y){
    return x*y/gcd(x,y);
}
inline void init()
{
    memset(f,-1,sizeof(f));
    cnt=0;
    for (int i=1;i<=p;i++)
        if (p%i==0) cnt++,num[i]=cnt;
}
inline int dfs(int pos,int sum,int lcm,int limit)
{
    if (!pos) return sum%lcm==0;
    if (!limit&&f[pos][sum][num[lcm]]!=-1)
        return f[pos][sum][num[lcm]];
    int res=limit?a[pos]:9,ret=0;
    for (int i=0;i<=res;i++)
    {
        int nxt_sum=(sum*10+i)%p;
        int nxt_lcm=lcm;
        if (i) nxt_lcm=LCM(nxt_lcm,i);
        ret+=dfs(pos-1,nxt_sum,nxt_lcm,limit&&i==res);
    }
    if (!limit) f[pos][sum][num[lcm]]=ret;
    return ret;
}
inline int solve(int x)
{
    len=0;
    while(x) a[++len]=x%10,x/=10;
    return dfs(len,0,1,1);
}
signed main()
{
    init();
    scanf("%lld",&T);
    while(T--)
    {
        scanf("%lld%lld",&l,&r);
        printf("%lld\n",solve(r)-solve(l-1));
    }
    return 0;
}

T2 rps

题目大意:有$n$个人玩石头剪刀布,给定每个人一直出的拳。赢的进入下一轮。要求找出字典序最小的方案使得游戏能够结束。如果没有方案输出$-1$。

知道每个人出的拳之后就可以知道谁赢了。晋级关系构成一棵树,不妨枚举树根最后是谁赢了,然后递归暴力比较字典序即可。

时间复杂度$O(n)$。

代码:

#include<bits/stdc++.h>
using namespace std;
int r[3],n=0;
inline int read()
{
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
    while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
char s[]="RPS";
string solve(int x,int y){
    if(!x){
        string ans;
        ans.push_back(s[y]);
        return ans;
    }
    string a=solve(x-1,y),b=solve(x-1,(y+1)%3);
    return a<b?a+b:b+a;
}
bool judge(int x){
    int a[3]={},aa[3];
    a[x]=1;
    for(int i=1;i<=n;i++){
        for(int j=0;j<=2;j++) aa[j]=a[j];
        for(int j=0;j<=2;j++) a[(j+1)%3]+=aa[j];
    }
    for(int i=0;i<=2;i++) if(a[i]!=r[i]) return 0;
    cout<<solve(n,x)<<endl;
    return 1;
}
int main(){
    r[0]=read(),r[1]=read(),r[2]=read();
    while((1<<n)!=r[0]+r[1]+r[2]) n++;
    int i=0;
    for(;i<=2;i++){
        if(judge(i)==1) break;
    }
    if(i>2) cout<<"IMPOSSIBLE"<<endl;
    return 0;
}

T3 导弹拦截

题目大意:给定$n$个三元组$(x,y,z)$,表示导弹打的坐标;一个拦截系统拦截一个导弹后只能拦截$x,y,z$均比其小的导弹。问一次性最多拦截的导弹个数和所需要的最少的拦截系统。

第一问很水,直接最长上升子序列搞就行了。第二问我们可以抽象成一张有向无环图,边代表一个导弹对另一个导弹的“控制”(指$x,y,z$大小关系)。这时题意转变成求最小路径覆盖。4月份学二分图的时候博客里记了结论$ans=n-tot$($tot$指最大匹配),但没有证明。简单说一下:

一开始$n$个点看成$n$条不相交路径,没增加一个匹配等于将两个路径连到一起,路径数减少$1$。所以最少的路径数为$n-tot$。

时间复杂度$O(n^2)$。

代码:

#include<cstdio>
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1005;
int n,f[N],vis[N],c[N],ans,tot;
vector<int> v[N];
struct node{
    int x,y,z;
}a[N];
inline int read()
{
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
    while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
inline bool cmp(node a,node b)
{
    return a.x<b.x;
}
inline int dfs(int x)
{
    for (int i=0;i<v[x].size();i++)
    {
        int to=v[x][i];
        if (!vis[to])
        {
            vis[to]=1;
            if (!c[to]||dfs(c[to])){
                c[to]=x;
                return 1;
            }
        }
    }
    return 0;
}
int main()
{
    n=read();
    for (int i=1;i<=n;i++)
        a[i].x=read(),a[i].y=read(),a[i].z=read();
    for (int i=1;i<=n;i++)
    {
        f[i]=1;
        for (int j=0;j<i;j++)
        {
            if (a[i].x>a[j].x&&a[i].y>a[j].y&&a[i].z>a[j].z)
                v[j].push_back(i),vis[i]=1;
            else if (a[j].x>a[i].x&&a[j].y>a[i].y&&a[j].z>a[i].z)
                v[i].push_back(j),vis[j]=1;
        }
    }
    sort(a+1,a+n+1,cmp);
    for (int i=1;i<=n;i++)
        for (int j=0;j<i;j++)
            if (a[i].x>a[j].x&&a[i].y>a[j].y&&a[i].z>a[j].z)
                f[i]=max(f[i],f[j]+1);
    for (int i=1;i<=n;i++) ans=max(ans,f[i]);
    printf("%d\n",ans);
    for (int i=1;i<=n;i++)
        if (v[i].size()){
            memset(vis,0,sizeof(vis));
            tot+=dfs(i);
        }
    printf("%d",n-tot);
    return 0;
}

T4 符文师

题目大意:给定一个长度为$n$的序列,每个数都有一个值$d_i$和$l_i$,表示杀伤力和其后面不能选的数的个数。有两种操作:1.将第一个数放到最后;2.选这个数,连带着后面$l_i$个数都消掉。问最大杀伤力。

傻逼题。发现一个数的消去其实对于你想要的答案是没影响的,等于说现在有$n$个物品,体积为$l_i$,价值为$d_i$,你有一个大小为$n$的背包,然后求最大价值。直接01背包就完事了。

时间复杂度$O(n^2)$。

代码:

#include<cstdio>
#include<iostream>
using namespace std;
const int N=1005;
int f[N],w[N],v[N],n;
int main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++) scanf("%d",&w[i]);
    for (int i=1;i<=n;i++) scanf("%d",&v[i]);
    for (int i=1;i<=n;i++)
        for (int j=n;j>=w[i];j--)
            f[j]=max(f[j],f[j-w[i]]+v[i]);
    printf("%d",f[n]);
    return 0;
}

 

posted @ 2020-10-12 20:28  我亦如此向往  阅读(150)  评论(0编辑  收藏  举报