石子類問題總結

石子類問題總結

做題需要找興趣,比如我就比較喜歡做石子題


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;
} 
發佈了39 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章