石子类问题总结

9 篇文章 0 订阅
7 篇文章 0 订阅

石子类问题总结

做题需要找兴趣,比如我就比较喜欢做石子题


1.石子归并
Description
  你有一堆石头质量分别为W1,W2,W3…WN.(W<=100000)现在需要你将石头合并为两堆,使两堆质量的差为最小。
Input
  第一行为整数N(1<=N<=20),表示有N堆石子。接下去N行,为每堆石子的质量。
Output
  输出合并后两堆的质量差的最小值
Sample Input
  5
  5
  8
  13
  27
  14
Sample Output
  3

这是一个背包问题.
什么?
背包问题!
设sum=w1+w2+…+wn;
要使差变小,就要让两堆都向sum/2靠近,相当于有一个箱子容量为sum/2,同时有n个物品,每个物品有一个体积 (正整数)。要求从n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。(对,这就是NOIP2001普及组的题)

#include<cstdio>
#include<iostream>
using namespace std;
bool f[102000];
int n,a[21],sum;
int main()
{
    int i,j;
    cin>>n;
    //别问我下面这句话对不对 
    for(i=1;i<=n;i++) cin>>a[i],sum+=a[i];
    int tmp=sum/2;
    f[0]=true;
    for(i=1;i<=n;i++)
     for(j=tmp-a[i];j>=0;j--)//倒着来! 
       if(f[j]) f[j+a[i]]=true;
    for(i=tmp;i;i--) if(f[i]) break;
    cout<<sum-2*i;  
    return 0;
}

2.石子合并(一)
Description
  在一个操场上摆放着一行共n堆的石子。现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆石子数记为该次合并的得分。请编辑计算出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分。
Input
  输入第一行为n(n<=100),表示有n堆石子,第二行为n个用空格隔开的整数,依次表示这n堆石子的石子数量(<=1000)
Output
  输出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分
Sample Input
  3
  1 2 3
Sample Output
  9 11

dp!(NO 贪心)
f_max[i][j] 表示 把从i到j的石子合并的最大得分,f_min同理
f_max[i][j]=max{ f_max[i][k]+f_max[k+1][j],i<=j<=k-1 }+sum[j]-sum[i-1]
f_min[i][j]=min{ f_min[i][k]+f_min[k+1][j],i<=k<=j-1 }+sum[j]-sum[i-1]

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int maxn=110;
int s[maxn],f[maxn][maxn],i,j,k,n,x,g[maxn][maxn]; 
int min(int a,int b){return a>b? b:a;}
int main()
{   
    cin>>n;
    for(i=1;i<=n;i++) for(j=1;j<=n;j++)  g[i][j]=0;
    memset(f,127/3,sizeof(f));//特别大 ,多少呢? 707406378
    for(i=1;i<=n;i++) f[i][i]=0;
    for(i=1;i<=n;i++)
    {
        cin>>x;
        s[i]=s[i-1]+x;
    } 
    for(i=n-1;i>0;i--)
      for(j=i+1;j<=n;j++)
        for(k=i;k<=j-1;k++)
        {
            g[i][j]=max(g[i][j],g[i][k]+g[k+1][j]+s[j]-s[i-1]);
            f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
        }
    cout<<f[1][n]<<" "<<g[1][n]<<endl;
    return 0;
}

3.石子合并(二)
Description
  在一个园形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。试设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分。
Input
  输入数据的第1行试正整数N,1≤N≤1000,表示有N堆石子.第2行有N个数,分别表示每堆石子的个数。
Output
  输出共2行,第1行为最小得分,第2行为最大得分.
Sample Input
  4
  4 4 5 9
Sample Output
  43
  54

难道和上一个不一样?
不一样,这是一个环
要是能把环变成直线,再用刚才的动态转移方程就ok啦
for(i=1;i<=n;i++) cin>>a[i],a[i+n]=a[i];
这样n堆石子就变成了2n堆,环就变成了直线
maxn=max(f_max[i][i+n-1],1<=i<=n),
minn=min(f_min[i][i+n-1],1<=i<=n)
Attention!时间复杂度O(n^3) 超时!
需要动态规划优化到O(n^2)
求最小值的话用平行四边形优化:
设p[i][j]表示把从i到j堆石子合并时k的取值,k就是要合并的位置
f_min[i][j]=max{ f[i][j],f_min[i][k]+f_min[k+1][j]+sum[j]-sum[i-1], p[i][j-1]<=k<=p[i+1][j] }
求最大值的话有这样一个结论:f_max[i][j]=max{ f_max[i+1][j],f_max[i][j-1])+sum[j]-sum[i-1]; } 证明略

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int n,f_max[2001][2001],f_min[2001][2001],i,j,k,a[2001],sum[2002],maxn,minn=1000000,p[2001][2010];
int main()
{
    memset(f_min,127/3,sizeof(f_min));
    cin>>n;
    for(i=1;i<=n;i++) cin>>a[i],a[i+n]=a[i];
    for(i=1;i<=2*n;i++) sum[i]=sum[i-1]+a[i],f_min[i][i]=0,s[i][i]=i,p[i][i]=i;
    for(i=2*n-1;i;i--)
      for(j=i+1;j<=2*n;j++)  
           {
              f_max[i][j]=max(f_max[i+1][j],f_max[i][j-1])+sum[j]-sum[i-1];
           }
    for(i=2*n-1;i;i--)
      for(j=i+1;j<=2*n;j++)
         for(k=p[i][j-1];k<=p[i+1][j];k++)
           {
              if(f_min[i][j]>f_min[i][k]+f_min[k+1][j]+sum[j]-sum[i-1]) 
                f_min[i][j]=f_min[i][k]+f_min[k+1][j]+sum[j]-sum[i-1],p[i][j]=k;
           }
    for(i=1;i<=n;i++)
    {
        maxn=max(f_max[i][i+n-1],maxn);
        minn=min(minn,f_min[i][i+n-1]);
    }       
    cout<<minn<<endl<<maxn;
} 

4.SDOI2008石子合并
Description
  在一个操场上摆放着一排N堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。
  试设计一个算法,计算出将N堆石子合并成一堆的最小得分。
Input
  第一行是一个数N。
  以下N行每行一个数A,表示石子数目。
Output
  共一个数,即N堆石子合并成一堆的最小得分。
Sample Input
  4
  1
  1
  1
  1
Sample Output
  8
Hint
【数据规模和约定】
  对于 30% 的数据,1≤N≤100
  对于 60% 的数据,1≤N≤1000
  对于 100% 的数据,1≤N≤40000
  对于 100% 的数据,1≤A≤200

哈哈 O(n^2)也过不了
GarsiaWachs算法登场 看代码吧

/*
GarsiaWachs算法的流程: 
【假设a[0]=a[n+1]=inf】 
1.从序列的左端开始找第一个a[k-1]≤a[k+1]的k,然后合并a[k-1],a[k]
2.从当前位置开始向左找第一个a[j]>a[k-1]+a[k]的j,把合并后的值插到j的后面,没有就当第一个 
3.一直这样重复下去直到剩下一堆
*/ 
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
long long ans;
int n,a[50100],t;
void work(int x)
{
    int i,j;
    int tmp=a[x]+a[x-1];
    ans+=tmp;
    for( i=x;i<t-1;i++) a[i]=a[i+1];//后面的数向前一位 
    t--;
    for( j=x-1;j>0&&a[j-1]<tmp;j--) a[j]=a[j-1];
    a[j]=tmp; 
    //注意下面的循环,算法每次是从左往右找第一个k,而找到并归位以后 有可能在前面 出现新的满足条件的k 
    while(j>=2&&a[j]>=a[j-2]) {
       int d=t-j;work(j-1);j=t-d;}
}
int main()
{
    int i,j;
   cin>>n;
   for(i=0;i<n;i++) scanf("%d",&a[i]);
   t=1;
   for(i=1;i<n;i++) 
   {
     a[t++]=a[i];
     while(t>=3&&a[t-3]<=a[t-1]) work(t-2);
   }
   while(t>1) work(t-1);
   cout<<ans;
   return 0;
}

5.质数取石子
Description
  DD 和 MM 正在玩取石子游戏。他们的游戏规则是这样的:桌上有若干石子,DD 先取,轮流取,每次必须取质数个。如果某一时刻某一方无法从桌上的石子中取质数个,比如说剩下 0 个或 1 个石子,那么他/她就输了。

  DD 和 MM 都很聪明,不管哪方存在一个可以必胜的最优策略,他/她都会按照最优策略保证胜利。于是,DD 想知道,对于给定的桌面上的石子数,他究竟能不能取得胜利呢?

  当 DD 确定会取得胜利时,他会说:“不管 MM 选择怎样的取石子策略,我都能保证至多 X 步以后就能取得胜利。”那么,最小的满足要求的 X 是多少呢?注意,不管是 DD 取一次石子还是 MM 取一次石子都应该被计算为“一步”。

Input
  第一行有一个整数 N,表示这个输入文件中包含 N 个测试数据。

  第二行开始,每行有一个测试数据,其中仅包含一个整数,表示桌面上的石子数。
Output
  你需要对于每个输入文件中的 N 个测试数据输出相应的 N 行。

  如果对于该种情形是 DD 一定取得胜利,那么输出最小的 X。否则该行输出 -1。
Sample Input
  3
  8
  9
  16
Sample Output
  1
  -1
  3

Hint
  【样例说明】
    当桌上有 8 个石子时,先取的 DD 只需要取走 7 个石子剩下 1 个就可以在一步之后保证胜利,输出 1。
    当桌上有 9 个石子时。若 DD 取走 2 个,MM 会取走 7 个,剩下 0 个,DD 输。若 DD 取走 3 个,MM 会取走 5 个,剩下 1 个,DD 输。DD 取走 5 个或者 7 个的情况同理可知。所以当桌上有 9 个石子时,不管 DD 怎么取,MM 都可以让 DD 输,输出 -1。
    当桌上有 16 个石子时,DD 可以保证在 3 步以内取得胜利。可以证明,为了在 3 步内取得胜利,DD 第一步必须取 7 个石子。剩下 9 个石子之后,不管第二步 MM 怎么取,DD 取了第三步以后可以保证胜利,所以输出 3。

  【数据范围】
    输入文件中的数据数 N<=10。
    每次桌上初始的石子数都不超过 20000。
 
有没有博弈论的感觉?
没有,还是dp
g[i]表示当桌上还有i个石子时接下来取的人赢(true)还是输(false)
g[i]=true 当且仅当 存在prime[j]满足g[i-prime[j]]=false
若不存在则g[i]=false
输出步数比较麻烦(以下废话)
因为DD知道了自己会赢,他就会哈皮的和MM说:“我最多在x步之内赢你。”
而MM听到DD这么说,MM还就不信这个邪,她就会每次尽量少取,让DD多取几回
而DD知道MM会这样取,DD为了自己的尊严不受到践踏,他就会每次尽量多取
然而他不知道x最小是多少,他就找到了会编程的你

#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
using namespace std;
int g[20020],prime[2263],cnt,sp[20020];
int step(int x)
{
    memset(sp,0,sizeof(sp));
    for(int i=2;i<=x;i++)
    {
        for(int j=1;j<=cnt;j++)
        {
            if(x<prime[j]) break;
            if(g[i]==1&&g[i-prime[j]]==-1) if(sp[i]==0) sp[i]=sp[i-prime[j]]+1;else sp[i]=min(sp[i],sp[i-prime[j]]+1);
            if(g[i]==-1&&g[i-prime[j]]==1) if(sp[i]==0) sp[i]=sp[i-prime[j]]+1;else sp[i]=max(sp[i],sp[i-prime[j]]+1);
        }
    }
    return sp[x];
}
int work(int x)
{
    memset(g,0,sizeof(g));
    g[0]=g[1]=-1;
    for(int i=2;i<=x;i++)
    {
        bool flag=false;
        for(int j=1;j<=cnt;j++)
        {
            if(prime[j]>x) break;
            if(g[i-prime[j]]==-1) {  flag=true; g[i]=1; break;  }//必胜
        }
        if(!flag) g[i]=-1;//必败
    }
    return g[x];
}
void makeprime()
{
    for(int i=2;i<=20000;i++)
    {
        bool tmp=false;
        for(int j=2;j<=sqrt(i);j++) if(i%j==0) tmp=true;
        if(!tmp) prime[++cnt]=i;
    }
}
int main()
{
   makeprime();
   int n,i,x,f;
   cin>>n;
   for(i=1;i<=n;i++)
   {
    cin>>x;
    f=work(x);
    if(f==-1) cout<<-1<<endl;
    if(f==1) cout<<step(x)<<endl;
   }
   return 0;
}

6.取石子
Description
有n个石子围成一圈,每个石子都有一个权值a[i],你需要取一些石子,每个石子的得分是a[i]*d,d表示这个石子到两边被取了的石子的距离和。
现在你可以取若干石子,使总得分最大。
Input
第1行一个整数n。
接下来n行,每行一个整数a[i]。
Output
仅一个整数,表示最大得分。
Sample Input
5
1
2
3
4
20
Sample Output
80

【样例解释】
取出20后,20旁的石头只剩一侧,长度为4,20*(0+4)=80.距离指两粒石子之间的石子数
Hint
【数据规模】
1≤a[i]≤100000
对于30%的数据,n≤60
对于60%的数据,n≤300
对于100%的数据,n≤100000

这个题需要好好理解,再自己算一算
首先可以肯定的是,当石子个数相同时,权值越大,分数越大
eg: 1 2 3 4 20
取20时的分数是80,取4和20时的分数为 4*(3+0)+20*(0+3)=72
你会发现取的越多分数反而越小
呃~例子不好再找一个
eg: 1 18 3 19 20
取20时的分数是80,
取19和20时的分数为19*(3+0)+20*(0+3)=117>80,
取 18,19,20时的分数为18*(2+1)+19*1+20*(0+1)=93<117
你会发现取的石子超过两个 分数就会变低,而取一个还是取两个这是一个问题
直接在程序里比较一下就行了
找到最大值max1和次大值max2(不需要排序)
ans=max( max1*(n-1),(max1+max2)*(n-2) )

#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;

int main()
{
    long long n,i,x;
    long long max1,max2;
    cin>>n;
    if(n>=2) cin>>max1>>max2;else cin>>max1;
    if(max1<max2) swap(max1,max2);
    for(i=3;i<=n;i++)//寻找最大值和次大值
    {
        scanf("%lld",&x);
        if(x>max1) {
            max2=max1;max1=x;
        }else
        if(x>max2) max2=x;
    }
//    long long a[100010];
//    for(i=1;i<=n;i++) scanf("%d",&a[i]);
//    sort(a+1,a+n+1);
//    max1=a[n];max2=a[n-1];
    long long ans1=max1*(n-1);
    long long ans2=(max1+max2)*(n-2);
    printf("%lld",max(ans1,ans2));
   return 0;
}

7.取石子游戏(一)
Description
有一种有趣的游戏,玩法如下
玩家:2人
道具:N颗石子
规则:
1.游戏双方轮流取石子;
2.每人每次取走若干颗石子(最少取1颗,最多取K颗);
3.石子取光,则游戏结束;
4.最后取石子的一方为胜;
假如参与游戏的玩家都非常聪明,问最后谁会获胜?
Input
一行,两个整数N和K。(1<=N<=100000,K<=N)
Output
一行, 一个整数,若先手获胜输出1,后手获胜输出2
Sample Input
23 3
Sample Output
1

博弈论基础
还需要再解释什么吗?

#include<cstdio>
#include<iostream>
using namespace std;
int main()
{
    int n,k;
   cin>>n>>k;
   if(n%(k+1)) cout<<1;else cout<<2;
   return 0;
}

8.取石子游戏(二)
Description
有一种有趣的游戏,玩法如下
玩家:2人
道具:N堆石子,每堆石子的数量分别为X1,X2,…,Xn
规则:
1.游戏双方轮流取石子;
2.每人每次选一堆石子,并从中取走若干颗石子(至少取1颗);
3.所有石子被取完,则游戏结束;
4.如果轮到某人取时已没有石子可取,那此人算负;

假如两个游戏玩家都非常聪明,问谁胜谁负?
游戏试玩(只有三堆的情况):
Input
第一行,一个整数N(N<=10000)
第二行,N个空格间隔的整数Xi,表示每一堆石子的颗数(1<=Xi<=100000)。
Output
一行, 一个整数,若先手获胜输出1,后手获胜输出2
Sample Input
4
7 12 9 15
Sample Output
1

经典Nim博弈


#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int main()
{
    int n,x,f;
    cin>>n>>f;
    for(int i=1;i<n;i++)
    {
        cin>>x;
        f=f^x;
    }
    if(!f) cout<<'2';else cout<<'1';
    return 0;
} 
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值