NOI 2002 题解

贪吃的九头龙

(传送门)

题意

N个节点的一棵树被M个脑袋吃,每个脑袋至少吃一个。最大的头必须恰好吃K个且必须包括1号节点。如果一条树边的两边都是被同一个脑袋吃掉的,则这段树枝的权值将被计算进答案中,要求使答案最小。

分析

此题是一道Tree Dp

先判断是否无解,当果子不够吃时,即N-K<M-1则无解。

非无解情况M个脑袋分N个点,其中一个还必须分得K个且包括1,有些条件必须简化掉,把M个脑袋简化成一个脑袋吃K个的最小代价就好了,可以分两种情况:

1、M=2,就是大头吃掉的树枝+小头吃掉的树枝。

2、M>2,此时只需考虑大头吃掉的树枝,因为其他的可以根据奇偶分给不同的头吃,使他们不被算入最终答案。

先把树转成二叉树,定义f[i][j][k]为 i 为根的子树分 j 个给大头吃,父亲是k(k=1被大头吃k=0被小头吃)。则有:

f[i][j][k]=min{ f[lc[i]][X][0]+f[rc[i]][j-X][k]+(m==2)*(k==0)* d[i]  ||  f[lc[i]][X-1][1]+f[rc[i]][j-X][k]+(k==1)*d[i]  }

代码

#include <bits/stdc++.h>
using namespace std;

const int MAXN=300+5;
const int INF=0x3f3f3f3f;

int N,M,K;
int sz[MAXN],f[MAXN][MAXN][3];
struct node
{
    int lc,rc,d;
} tree[MAXN];

void dfs(int root)
{
    if(!root) return ;
    dfs(tree[root].lc);
    dfs(tree[root].rc);
    sz[root]=sz[tree[root].lc]+sz[tree[root].rc]+1;
}

int dp(int root,int x,int g)
{
    if(x<0) return INF;
    if(f[root][x][g]>=0) return f[root][x][g];
    if(!root && !x) return f[root][x][g]=0;
    
    f[root][x][g]=INF;
    for(int i=0;i<=min(x,sz[root]);i++)
    {
        int t1=dp(tree[root].lc,i,0)+(M==2)*(g==0)*tree[root].d;
        int t2=dp(tree[root].lc,i-1,1)+(g==1)*tree[root].d;
        int t3=dp(tree[root].rc,x-i,g);
        t1=min(t1,t2);
        f[root][x][g]=min(f[root][x][g],t1+t3);
    }
    return f[root][x][g];
}

int main()
{
    cin>>N>>M>>K;
    if(N-K<M-1)
    {
        cout<<-1<<endl;
        return 0;
    }
    for(int i=1;i<N;i++)
    {
        int x,y,d;
        scanf("%d%d%d",&x,&y,&d);
        tree[y].rc=tree[x].lc;
        tree[x].lc=y;
        tree[y].d=d;
    }

    dfs(1);

    memset(f,0x3f,sizeof(f));
    printf("%d\n",dp(tree[1].lc,K-1,1));

    return 0;
}


银河英雄传说

(传送门)

题意

对于一些互相独立的战舰,有以下两种操作。

1, M i j :让第i号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第j号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。
2, C i j :询问第i号战舰与第j号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。

分析

此题是一道比较裸的并查集

num[i]表示所在 i 集合中战舰的总个数,before[i]表示i所在集合中在i之前有多少个战舰

对于每次查询,调用find()函数判断是否在同一列,如果在abs(before[a]-before[b])-1即为答案

代码中注释的一步需要注意顺序,因为并查集涉及到了路径压缩

代码

#include <bits/stdc++.h>
using namespace std;

const int MAXN=30000+10;
int fa[MAXN];
int num[MAXN];//表示所在集合中战舰的总个数
int before[MAXN];//before[i]表示i所在集合中在i之前有多少个战舰

int find(int a)
{
    if(a==fa[a]) return a;
    else
    {//这里面的顺序不能变化,这步是伴随着路径压缩从根逐层向下更新before值
        int tmp=find(fa[a]);
        before[a]+=before[fa[a]];
        fa[a]=tmp;
        return tmp;
    }
}

void unio(int a,int b)
{
    int x=find(a),y=find(b);
    fa[x]=y;
    before[x]+=num[y];
    num[y]+=num[x];
}

int main()
{
    for(int i=1;i<MAXN;i++)
    {
        fa[i]=i;
        before[i]=0;
        num[i]=1;
    }
    int T;
    cin>>T;
    while(T--)
    {
        char s[3];
        int a,b;
        scanf("%s%d%d",s,&a,&b);
        if(cmd[0]=='M') unio(a,b);
        else
        {
            if(find(a)!=find(b)) printf("-1\n");
            else printf("%d\n",abs(before[a]-before[b])-1);
        }
    }
    return 0;
}


荒岛野人

(传送门)

题意

M个山洞,编号为1,2…MN个人,开始依次住在山洞C1,C2,…,CN中,以后每年,第i个野人会沿顺时针向前走Pi个洞住下来。每个野人i有一个寿命值Li,即生存的年数。没有任何两个野人在有生之年处在同一个山洞中,求至少有多少个山洞。

分析

数学题,求解使用扩展欧几里得(ex_gcd)

对于两个野人 i , j 的情况。如果 i , j 在 x 年相遇并且在第 x 年时俩人都活着,有C[i]+x*P[i]=C[j]+x*P[j] (mod m) ,要求不能使野人们相遇,则需要让这个同余方程无解,或解出的最小的x比两个人中任何一人的寿命长(最小正整数解>min(L[i],L[j])),可以用exgcd求解,即(P[i]-P[j])*x-my=C[j]-C[i]

对于n个野人呢,由于n<=15,M的初始值为max{c[i]}若有一对能相遇,则这个M不可行,需要M++,直到找到一个M,使每两个野人都不能相遇。

代码

#include <bits/stdc++.h>
using namespace std;

int n,C[20],P[20],L[20];

int gcd(int a,int b)
{
    return b==0 ? a : gcd(b,a%b);
}

void exgcd(int a,int b,int &x,int &y)
{
    if(b==0)
    {
        x=1;
        y=0;
        return;
    }
    exgcd(b,a%b,x,y);
    int t=x;
    x=y;
    y=t-a/b*y;
}

bool ok(int m)
{
    for(int i=1;i<=n;i++)
        for(int j=i+1;j<=n;j++)
        {
            int a=P[i]-P[j],b=m,c=C[j]-C[i];
            int x,y,t=gcd(a,b);
            if(c%t==0)
            {
                a/=t;b/=t;c/=t;
                exgcd(a,b,x,y);
                b=abs(b);
                x=((c*x)%b+b)%b;
                if(!x) x+=b;
                if(x<=min(L[i],L[j])) return 0;
            }
        }
    return 1;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d%d",&C[i],&P[i],&L[i]);
        C[0]=max(C[i],C[0]);
    }
    for(int i=C[0];;i++)
        if(ok(i))
        {
            cout<<i<<endl;
            return 0;
        }
}

机器人M号

(传送门)

题意

第m秒造出的机器人编号为m。我们可以称它为机器人m号,或者m号机器人。机器人造出来后,马上开始工作。m号机器人,每m秒会休息一次。比如3号机器人,会在第6,9,12,……秒休息,而其它时间都在工作。机器人休息时,它的记忆将会被移植到当时出生的机器人的脑中。比如6号机器人出生时,2,3号机器人正在休息,因此,6号机器人会收到第2,3号机器人的记忆副本。我们称第2,3号机器人是6号机器人的老师。一个机器人的独立数,是指所有编号比它小且与它知识互相独立的机器人的个数。比如1号机器人的独立数为0,2号机器人的独立数为1(1号机器人与它知识互 相独立),6号机器人的独立数为2(1,5号机器人与它知识互相独立,2,3号机器人都是它的老师,而4号机器人与它有共同的老师——2号机器人)。机器人有3种不同的职业。对于编号为m的机器人,如果能把m分解成偶数个不同奇素数的积,则它是政客,例如编号15;否则,如 果m本身就是奇素数或者能把m分解成奇数个不同奇素数的积,则它是军人,例如编号 3, 编号165。其它编号的机器人都是学者,例如编号2, 编号6, 编号9。机器人m号,想知道它和它的老师中,所有政客的独立数之和,所有军人的独立数之和,以及所有学者的独立数之和。

分析

数论问题,欧拉函数

机器人M号的所有老师就是小于M大于1M的所有约数,M号机器人的独立数就是小于M于M互质的数的个数,即为M的欧拉函数φ(M)。把M分解为P1^e1P2^e2...*Pk^ek,欧拉函数有公式为:φ(M)=(P1-1)P1^(e1-1) (P2-1)P2^(e2-1) ... (Pk-1)Pk^(ek-1)

求政客和军人各自的独立数之和,可递推。另f[i]为M的所有大于2的质因数中的选择i个质因数的欧拉函数和。

f[j]=f[j]+f[j-1]*(p[i]-1) 。“政客”的独立数之和就是∑F[i] (i为偶数),“军人”的独立数之和就是∑F[i] (i为奇数)。

学者用小于M的M的所有约数的独立数之和减去政客和军人的独立数即可。

注意会爆long long,所以要写快速乘法。

代码

#include <bits/stdc++.h>
using namespace std;

const int MAXK=1000+5;
const int MOD=10000;
int p[MAXK],e[MAXK],f[MAXK];
int ans1,ans2,ans3,k;

int quick_power(int a,int k)
{
    int res=1;
    while(k)
    {
        if(k&1) res=(res*a)%MOD;
        k>>=1;
        a=a*a%MOD;
    }
    return res%MOD;
}

int main()
{
    cin>>k;
    for(int i=1;i<=k;i++)
        scanf("%d%d",&p[i],&e[i]);
    int a=(p[1]==2) ? 2 : 1;
    f[0]=1;
    for(int i=a;i<=k;i++)
        for(int j=i-a+1;j>=1;j--)
            f[j]=(f[j]+f[j-1]*(p[i]-1))%MOD;
    for(int i=1;i<=k-a+1;i++)
        if(i%2) ans2=(ans2+f[i])%MOD;
        else ans1=(ans1+f[i])%MOD;
    int res=1;
    for(int i=1;i<=k;i++)
        res=(res*quick_power(p[i],e[i]))%MOD;
    ans3=(res-ans1-ans2-1+MOD+MOD)%MOD;

    cout<<ans1<<endl<<ans2<<endl<<ans3<<endl;
    return 0;
}



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值