在去年noip集训的时候,按照老师的要求,我每次考试都写了一份题解,但是没发到网上来。。。
现在抽点空余时间整理一下发出来,也算是一种复习吧。。
先发题目:
------------------------------------------------------------------------------------------------------------------------------------------------
一、质数取石子
match.cpp/c/pas
Time Limit: 1S Memory Limit: 64M
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
二、数字游戏
game.cpp/c/pas
Time Limit: 1 s Memory Limit: 32 MB
Description
小W发明了一个游戏,他在黑板上写出了一行数字a1,a2,….an,然后给你m个回合的机会,每回合你可以从中选择一个数擦去它,接着剩下来的每个数字ai都要递减一个值bi。如此重复m个回合,所有你擦去的数字之和就是你所得到的分数。
小W和他的好朋友小Y玩了这个游戏,可是他发现,对于每个给出的an和bn序列,小Y的得分总是比他高,所以他就很不服气。于是他想让你帮他算算,对于每个an和bn序列,可以得到的最大得分是多少。这样他就知道有没有可能超过小Y的得分。
Input
从文件game.in中读入数据。
第一行,一个整数n(1<=n<=200),表示数字的个数。
第二行,一个整数m(1<=m<=n),表示回合数。
接下来一行有n个不超过10000的正整数,a1,a2…an,表示原始数字
最后一行有n个不超过500的正整数,b1,b2….bn,表示每回合每个数字递减的值
Output
输出到文件game.out中,一个整数,表示最大可能的得分
Sample Input:
3
3
10 20 30
4 5 6
Sample output:
47
三、昂贵的聘礼
dowry.cpp/c/pas
Time Limit: 1000MS Memory Limit: 10000K
Description
年轻的探险家来到了一个印第安部落里。在那里他和酋长的女儿相爱了,于是便向酋长去求亲。酋长要他用10000个金币作为聘礼才答应把女儿嫁给他。探险家拿不出这么多金币,便请求酋长降低要求。酋长说:"嗯,如果你能够替我弄到大祭司的皮袄,我可以只要8000金币。如果你能够弄来他的水晶球,那么只要5000金币就行了。"探险家就跑到大祭司那里,向他要求皮袄或水晶球,大祭司要他用金币来换,或者替他弄来其他的东西,他可以降低价格。探险家于是又跑到其他地方,其他人也提出了类似的要求,或者直接用金币换,或者找到其他东西就可以降低价格。不过探险家没必要用多样东西去换一样东西,因为不会得到更低的价格。探险家现在很需要你的帮忙,让他用最少的金币娶到自己的心上人。另外他要告诉你的是,在这个部落里,等级观念十分森严。地位差距超过一定限制的两个人之间不会进行任何形式的直接接触,包括交易。他是一个外来人,所以可以不受这些限制。但是如果他和某个地位较低的人进行了交易,地位较高的的人不会再和他交易,他们认为这样等于是间接接触,反过来也一样。因此你需要在考虑所有的情况以后给他提供一个最好的方案。
为了方便起见,我们把所有的物品从1开始进行编号,酋长的允诺也看作一个物品,并且编号总是1。每个物品都有对应的价格P,主人的地位等级L,以及一系列的替代品Ti和该替代品所对应的"优惠"Vi。如果两人地位等级差距超过了M,就不能"间接交易"。你必须根据这些数据来计算出探险家最少需要多少金币才能娶到酋长的女儿。
Input
输入第一行是两个整数M,N(1 <= N <= 100),依次表示地位等级差距限制和物品的总数。接下来按照编号从小到大依次给出了N个物品的描述。每个物品的描述开头是三个非负整数P、L、X(X < N),依次表示该物品的价格、主人的地位等级和替代品总数。接下来X行每行包括两个整数T和V,分别表示替代品的编号和"优惠价格"。
Output
输出最少需要的金币数。
Sample Input
1 4
10000 3 2
2 8000
3 5000
1000 2 1
4 200
3000 2 1
4 200
50 2 0
Sample Output
5250
四、棋盘分割
chessboard.cpp/c/pas
Time Limit: 1S Memory Limit: 10M
Description
将一个8*8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了(n-1)次后,连同最后剩下的矩形棋盘共有n块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)
原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割成n块矩形棋盘,并使各矩形棋盘总分的均方差最小。
均方差,其中平均值,xi为第i块矩形棋盘的总分。
请编程对给出的棋盘及n,求出O'的最小值。
Input
第1行为一个整数n(1 < n < 15)。
第2行至第9行每行为8个小于100的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。
Output
仅一个数,为O'(四舍五入精确到小数点后三位)。
Sample Input
3
1 1 1 1 1 1 1 3
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 3
Sample Output
1.633
----------------------------------------------------------------------------------------------------
题解:
第一题:质数取石子、
大意是两个人轮流取石子,每次只能取质数个,没法继续取的一方就输了。也就是说,当一方取后只剩下1个或不剩了,另一方便输了。假定两人都很聪明,求当前保证要赢最少取多少次(必胜的情况)。
看到假设两人都很聪明,就说明这道题是博弈论的题了。博弈论的标准解题思路就是倒推,根据关系写出动规方程,这道题也不例外。
首先,当现在剩下的石子是0或1的时候,明显必输,而当剩下的石子数是一个质数或者质数+1的时候,则一步取胜。
关键的问题是讨论不属于这两种情况的石子数:当石子数通过一次取石子后可以转化为一个必输的情况(留给对方取),就必胜了,而且步数是那个必输情况的步数+1(如果有多种这样的取法,取其中使步数最小的情况);当不存在这样的取法时,就是必输。
那么必输的步数又是怎么确定的呢?当必输时,不论怎么取,都会得到一个必胜的情况(对方的),如果必输,那么就没有必要使步数最小了。因为题目要求,在必胜时求的值是能保证必胜的值。要保证必胜,就要考虑到转移到这个必胜情况的必输情况的所有可能步数(绕口?仔细断下句),取其中最大值(才能做到保证)。
于是这样,状态转移方程就可以写出来了:
开一个bool数组v[N],代表每种情况是否必胜,另开一个int数组dp[N],其中的值在必胜时代表当前保证要赢最少取多少次,在必败是代表最多步数;
方程即为:
V[0]=v[1]=0;
dp[0]=dp[1]=0;
V[i]=1;dp[i]=min(dp[j]+1);(v[j]=0&&j=i-质数或1)
V[i]=0;dp[i]=max(dp[j]+1);( 不存在v[j]=0&&j=i-质数或1)
代码:(用了一种等效但浪费空间的表示方法,但还是比较清晰,另外用到了一种高效的筛素数法)
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<algorithm>
using namespace std;
const int inf=0x3f3f3f3f;
int dp1[5000+10],dp2[5000+10];//dp1即必胜情况的最优答案,当必输时为-1,这时对应的dp2
//的值就是必输时的最大步数,其他情况下dp2貌似无意义
int prime[5000+10],l=0,n;
bool flag[5000+10];
int main()
{
freopen("match.in","r",stdin);
freopen("match.out","w",stdout);
scanf("%d",&n);
for(int i=2;i<=5000;i++)
{
if(!flag[i])prime[l++]=i;
for(int j=0;j<l&&prime[j]*i<=5000;j++)
{
flag[prime[j]*i]=1;
if(i%prime[j]==0)break;
}
}
for(int i=1;i<=5000;i++)//赋初值
{
dp1[i]=inf;
dp2[i]=-1;
}
dp1[0]=dp1[1]=-1;
dp2[0]=dp2[1]=0;
for(int i=2;i<=5000;i++)
{
for(int j=0;j<l&&prime[j]<=i;j++)
{
if(dp1[i-prime[j]]==-1)
{
dp1[i]=min(dp1[i],dp2[i-prime[j]]+1);
}
else dp2[i]=max(dp2[i],dp1[i-prime[j]]+1);
}
if(dp1[i]==inf)
{
dp1[i]=-1;
}
// printf("%d :%d %d\n",i,dp1[i],dp2[i]);
}
while(n--)
{
int i;
scanf("%d",&i);
printf("%d\n",dp1[i]);
}
return 0;
}
第二题:数字游戏
又是两个无聊的人在玩游戏。。。。
题意我就不说了,当时我写这道题的时候看错了题,但是自己也觉得不对,就一直在纠结,最后终于看清题了,但是写了个搜索,TLE了。
题意看懂了后,其实很容易就会想到一个贪心的性质:把递减最快的先取,就可能得到一个最优解。但是再细想,会发现这样一个问题:递减快的不一定要取才是最优情况,不取时它递减再快也无影响。
于是,我们就否定了直接的贪心,但是这个思路并没有死。(我当时就是想到这就没继续了)
原因?我们先说这道题的正解:把数字案递减速度从大到小排序,回合m当做背包容量,分数抽象成物品价值,转换成一个01背包问题。
这样为什么就是正确的呢?因为01背包的模型完全考虑了所有物品取与不取的情况,而先考虑递减快的数字又保证了最小程度的浪费。背包模型之所以经典,也与这个全局考虑的性质有关。
状态转移方程:
F[i][j]=max(F[i-1][j],F[i-1][j-1]+a[i]-(j-1)*b[i]);(空间上可优化一维)
到这里,本题被成功解决了。
(其实到现在我又有新的理解了:因为在一个选择的序列中,肯定是把递减最快的最先取最好。所以一开始就先排好序再来选,选到的所用序列都是选相同的序列的最优情况)
#include<cstdio>
#include<cmath>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
struct E
{
int a,b;
bool operator < (const E B)const
{
return b>B.b;
}
}s[200+10];
int n,m;
int dp[200+10][200+10];
int main()
{
freopen("game.in","r",stdin);
freopen("game.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&s[i].a);
for(int i=1;i<=n;i++)scanf("%d",&s[i].b);
sort(s+1,s+n+1);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]+s[i].a-s[i].b*(j-1));
}
printf("%d\n",dp[n][m]);
return 0;
}
第三题:昂贵的聘礼
本题早在寒假时我就已经见过(poj稀有且序号靠前的中文题目),并且也做过,但当时由于拙计的代码能力,一直没过。
再见到这道题,我渐渐的回想起来当时的思路以及网上的题解。算法:最短路+枚举。
但是我又貌似看到了另一种解法:树形dp,于是便从这条路走了下去,光荣跪掉。
后来我又分析了很久,到底能不能用树形dp还是没理清,等到以后实力再提升后再来考虑吧。
说说正解:既然知道是最短路,我没有直接往这条路走的原因是还没有明显看出最短路的模型。这里建图时需要虚拟一个起点,然后把这个点往每个物品点连一条有向边,边权为物品的原价值,然后把各种调换关系也用有向边表示出来(从需要的物品指向换得的物品,权值为优化值)。这里最短路需要的起点,终点(一号物品),和图的模型就都出来了。
由于有等级限制,这道题变得稍显困难了。有人的第一思维是在最短路算法过程中去处理这些等级矛盾,但是图论的算法都是前人总结的很经典巧妙的算法,要想在其中修改一些东西来达到新功能,必须要对这些算法有深刻的理解,而且要时间充足。除非你是神牛,否则不要轻易走这条路,走到后面会越来越复杂(我第一次写好像就绕进去了);
然后第二种想法是:把和酋长等级超过限制的点删去,在得到的新图一次dijkstra算法(或其他的最短路算法)即可把问题解决。虽然按照常理,酋长应该是部落里最高等级的人,但是题中没有说明,也就说不一定。这样,比酋长等级高的点和低的点可能都可与酋长交易,但是他们之间可能是不能间接交易的,这种想法也不完全对。
那么正解呢?在第二种想法的基础上,把限制等级的方法改成明确的范围,超出等级范围的点计算时暂时忽略,枚举每次的范围,进行多次最短路计算,取其中最小值,题目解决。
#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<algorithm>
#include<iostream>
#include<queue>
using namespace std;
const int inf=0x3f3f3f3f;
int m,n;
struct E
{
int v,w,next;
}e[101*101+10];
int head[100+10],d[100+10],t=0;
int l[100+10];
bool vis[100+10];
struct cmp
{
bool operator()(const int a,const int b)
{
return d[a]>d[b];
}
};
void dij(int ll,int r)//与下面的spfa二选一就好了
{
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++)d[i]=inf;
d[0]=0;
priority_queue<int ,vector<int> ,cmp> q;
q.push(0);
while(!q.empty())
{
int x=q.top();
q.pop();
if(vis[x])continue;
vis[x]=true;
for(int i=head[x];i!=-1;i=e[i].next)
{
if(l[e[i].v]>=ll&&l[e[i].v]<=r)
{
if(d[e[i].v]>d[x]+e[i].w)
{
d[e[i].v]=d[x]+e[i].w;
q.push(e[i].v);
}
}
}
}
}
void spfa(int ll,int r)
{
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++)d[i]=inf;
d[0]=0;vis[0]=true;
queue<int> q;
q.push(0);
while(!q.empty())
{
int x=q.front();
q.pop();
vis[x]=false;
for(int i=head[x];i!=-1;i=e[i].next)
{
if(l[e[i].v]>=ll&&l[e[i].v]<=r)
{
if(d[e[i].v]>d[x]+e[i].w)
{
d[e[i].v]=d[x]+e[i].w;
if(!vis[e[i].v])q.push(e[i].v);
vis[e[i].v]=true;
}
}
}
}
}
void add(int u,int v,int w)
{
e[t].v=v;
e[t].w=w;
e[t].next=head[u];
head[u]=t++;
}
int main()
{
freopen("dowry.in","r",stdin);
freopen("dowry.out","w",stdout);
memset(head,-1,sizeof(head));
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)
{
int p,x,x1;
scanf("%d%d%d",&p,&l[i],&x);
add(0,i,p);
for(int j=1;j<=x;j++)
{
scanf("%d%d",&p,&x1);
add(p,i,x1);
}
}
int ans=inf;
for(int i=l[1]-m;i<=l[1];i++)
{
spfa(i,i+m);
if(ans>d[1])
ans=d[1];
}
printf("%d\n",ans);
return 0;
}
第四题:棋盘分割
这题的题意可能很多人都没理解到:右图之所以不合法,是因为每次切割后都先要舍弃一部分,不可以把两部分都拿来再次切割。
这题的状态其实很清晰:f[k][x1][y1][x2][y2],代表把以(x1,y1),(x2,y2)为对角顶点的矩形棋盘分成k份的最小(xi-ave)^2的和。因为ave是个定值(我当时由于时间紧迫没发现,就没写出来),所以每个状态都可直接计算出来,具有无后效性。
这道题状态维数如此多,把很多人都吓到了。维数多了状态转移顺序确实不好确定,这时用记忆化搜索的优势就显示了出来。当k==1时计算当前状态值,不等于1时继续分割就对了。(由于题目要求,只能分成f[k-1]和f[1](省略其他维数)。)
这样代码就很好写了。
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int inf=0x3f3f3f3f;
double dp[15+5][8+5][8+5][8+5][8+5];
bool vis[15+5][8+5][8+5][8+5][8+5];
int map[8+5][8+5],n;
double ave=0;
void dfs(int k,int x1,int y1,int x2,int y2)
{
if(vis[k][x1][y1][x2][y2])return;
vis[k][x1][y1][x2][y2]=true;
dp[k][x1][y1][x2][y2]=inf;
if(k==1)
{
int sum=0;
for(int i=x1;i<=x2;i++)
{
for(int j=y1;j<=y2;j++)
{
sum+=map[i][j];
}
}
dp[k][x1][y1][x2][y2]=(sum-ave)*(sum-ave);
return;
}
int s;
for(s=x1;s<=x2-1;s++)
{
dfs(1,x1,y1,s,y2);
dfs(k-1,x1,y1,s,y2);
dfs(k-1,s+1,y1,x2,y2);
dfs(1,s+1,y1,x2,y2);
dp[k][x1][y1][x2][y2]=min(dp[k][x1][y1][x2][y2],dp[1][x1][y1][s][y2]+dp[k-1][s+1][y1][x2][y2]);
dp[k][x1][y1][x2][y2]=min(dp[k][x1][y1][x2][y2],dp[k-1][x1][y1][s][y2]+dp[1][s+1][y1][x2][y2]);
}
for(s=y1;s<=y2-1;s++)
{
dfs(1,x1,y1,x2,s);
dfs(k-1,x1,y1,x2,s);
dfs(k-1,x1,s+1,x2,y2);
dfs(1,x1,s+1,x2,y2);
dp[k][x1][y1][x2][y2]=min(dp[k][x1][y1][x2][y2],dp[1][x1][y1][x2][s]+dp[k-1][x1][s+1][x2][y2]);
dp[k][x1][y1][x2][y2]=min(dp[k][x1][y1][x2][y2],dp[k-1][x1][y1][x2][s]+dp[1][x1][s+1][x2][y2]);
}
}
int main()
{
freopen("chessboard.in","r",stdin);
freopen("chessboard.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=8;i++)
for(int j=1;j<=8;j++)
{
scanf("%d",&map[i][j]);
ave+=map[i][j];
}
ave/=n;
dfs(n,1,1,8,8);
printf("%.3lf\n",sqrt(dp[n][1][1][8][8]/n));
return 0;
}