石子類問題總結
做題需要找興趣,比如我就比較喜歡做石子題
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;
}