NHOI2016解题报告
解题反思:这次比赛考得非常不好,第三题爆搜打错了,第五题没有想到用斜率来做,本来可以很高分的,下次继续努力。
南海实验中学 105班 区庆亮
第1题 购书(gs)
【题意分析】
有某人要买n本书。现在有促销活动,每买三本中可以去掉价格最小的一本再计算价钱。问:最合算的价格是多少?
【试题难度】
★☆☆☆☆
【解题思路】
贪心:
因为是取三本中价格最小的一本。所以,只有按照书的价格从大到小,每三本配套的时候,才能使价格最合算。所以,可以得出如下算法:
1, 按书的价格从大到小排序。
2, 每三本配套,计算能省下的价格。(也就是计算价格为3的倍数大的总和)
3, 输出总数-能省下的价格。
但是,值的注意的是,总数需要用long long。在最坏情况下,书的价格全部为100000,n=1000000。那么:价格总和就为(10^5)*(10^6)=10^11,要用longlong。
【时空复杂度】
时间复杂度:排序O(nlogn),计算价格O(n),总时间复杂度O(nlogn)。
空间复杂度:O(n)。
【解题反思】
做题时应该注意会不会超过int范围。写程序前,要看一看数据范围。
【程序】
#include<cstdio>
#include <iostream>
#include<algorithm>
using namespacestd;
int n,a[100011];
long long sum;//注意用long long
bool cmp(int x,inty){return x>y;}
int main()
{
freopen("gs.in","r",stdin);
freopen("gs.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
sum+=a[i];//计算总和
}
sort(a+1,a+n+1,cmp);//按从大到小排序
for(int i=3;i<=n;i+=3)
sum-=a[i];//计算能省的价钱
cout<<sum;
return 0;
}
第2题 最大最小(zdzx)
【题意分析】
输出l到d中各位数字的和为x的最小与最大的数。
【试题难度】
★☆☆☆☆
【解题思路】
只要纯模拟就可以了。分别顺序和倒叙顺序查找区间[l,d]中的书,只要找到第一个数的个位数字的和为x,直接输出那个数就可以了。
【时空复杂度】
时间复杂度: O(log10(D)*(D-L))
空间复杂度: O(1)
【解题反思】
要尽可能的把程序的复杂度优化,这样才能避免程序因为极端数据而超时。
【程序】
#include <cstdio>
#include <iostream>
using namespace std;
int n,m;
int x;
inline bool check(int x,int num)//x的个位数字的和是否为num
//如是,返回1。否则返回0.
{
int sum=0;
while(x>0)
{
sum+=x%10;
x/=10;
}
if(sum==num)return 1;
return 0;
}
int main()
{
freopen("zdzx.in","r",stdin);
freopen("zdzx.out","w",stdout);
scanf("%d%d%d",&n,&m,&x);
for(inti=n;i<=m;i++)//顺序查找
if(check(i,x))
{
printf("%d\n",i);
break;
}
for(inti=m;i>=n;i--)//倒序查找
if(check(i,x))
{
printf("%d\n",i);
break;
}
return 0;
}
第3题 组合数(zhs)
【题意分析】
从1到n中选取某些数,并加上限制条件,即某两个数不能选。输出方案总数。
【试题难度】
★★★☆☆
【解题思路】
一开始,我还以为是组合数学,但是看到n<=20时,就想到可以用搜索来解决。
首先,我们可以先用邻接矩阵f来存限制条件(当i,j之间有限制条件时,f[i][j]=true)。然后,我们可以在进行搜索。但是,搜索时,要注意:不能把所有的情况搜索出来再进行判断,要符合情况在进行搜索。但是,又要怎样判断呢?我们设当前数列为a,只需要判断当前搜到的数ak与a1~ak-1是否存在限制条件,即f[ak][ ai](0<i<k)都为0,才可以进行搜索。
【时空复杂度】
空间复杂度:O(n^2)
时间复杂度:O(2^n*n)
【解题反思】
1,假如考试时思绪很乱,就应该在草稿纸上列出所有的思路,记住每一步的细节,在进行编程。
2,搜索时,不能盲目地进行搜索。应该优化的地方一定要优化,避免出现没有用处的搜索方案,适当地进行剪枝。
【程序】
#include <cstdio>
using namespace std;
int n,m;
int ans=0;//因为方案总数最多为2^n,不会超过范围
int a[100010];
bool f[410][410];//邻接矩阵
int x,y;
void dfs(int t,int k,int now)//t为选择的个数,k为层数,now为现在的最大值 =a[k-1]+1;
{
if(k>t)//搜索到一种方案数
{
ans++;
return;
}
bool flag;
if(k==1)//特殊处理
{
for(inti=1;i<=n;i++)
{
a[k]= i;
dfs(t,k+1,i+1);
a[k]=0;
}
}
else
for(inti=now;i<=n;i++)
{
flag=0;
for(intj=1;j<k;j++)
if(f[a[j]][i]==1)//假如不符合条件
{
flag=1;
break;
}
if(flag)continue;
a[k] =i;
dfs(t,k+1,i+1);
a[k]=0;
}
}
int main()
{
freopen("zhs.in","r",stdin);
freopen("zhs.out","w",stdout);
scanf("%d%d",&n,&m);
for(inti=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
f[x][y]=f[y][x]=1;
}
ans=1;//当i=0时,集合为空
for(inti=1;i<=n;i++)
dfs(i,1,1);
printf("%d",ans);
return 0;
第4题 单词迷(dcm)
【题意分析】
有一个单词,然后把单词的字母打乱,连接到单词的后面。请找出长度最小的这样的单词并输出。如找不到则输出-1。
【试题难度】
★★★☆☆
【解题思路】
假如是连接到单词的后面,那么这个单词的长度必定是这个字符串的约数(不包括len)。然后,我们就可以枚举len所有的因子,再判断一下是否属于这个单词。那么,我们应该怎样判断呢?只需要把每个字符子串进行排序,再比较是否相等就可以了。
因子数最大为loglen,扫描所有整个字符串长度需要O(len),桶排时间总和约为O(len),所以,总时间复杂度为O(lenloglen ),不超时。
【时空复杂度】
时间复杂度:O(factor(len)*len)。//factor(len)指len因子的个数
空间复杂度:O(len)。
【解题反思】
尽量不要让程序进行没有用处的运算。需要想好程序的步骤再进行编程。
【程序】
#include<cstdio>
#include<cstring>
#include<string>
#include<iostream>
using namespacestd;
char sts[100010];
string s;
int n;
bool flag;
inline stringchange(string s)//排序 ,只要O(n)的时间
{
int a[30]={0},len=s.length();
for(int i=0;i<len;i++) a[s[i]-96]++;//桶排
string st;
for(int i=1;i<30;i++)
for(int j=1;j<=a[i];j++)
st+=char(i+96);
return st;
}
int main()
{
scanf("%s",sts);
s=sts;
n=s.length();
flag=0;
for(int i=1;i<n;i++)//特殊判断
{
if(s[i]!=s[0])
{
flag=1; break;
}
}
if(!flag)//假如全是一样
{
printf("%c",s[0]);
return 0;
}
for(int i=2;i<n;i++)
if(n%i==0)//枚举所有的因子
{
flag=1;
string now=change(s.substr(0,i));//根单词
for(int j=i;j<n;j+=i)
{
if(now!=change(s.substr(j,i)))//假如不一样
{
flag=0;
break;
}
}
if(flag)//符合条件
{
cout << s.substr(0,i);
return 0;
}
}
printf("-1");//无解
return 0;
}
第5题 线段(xd)
【题意分析】
有n在x轴上方的线段,现在要依次下落到x轴下方,且下落过程中不能与另外的线段相交,请求出一个下落过程。
【试题难度】
★★★★☆
【解题思路】
假如,线段x在下落过程中被条线段y挡住,那么,y就必须先输出。也就是说,不完成y就没法完成x,所以,我们就可以马上想到用拓扑排序!
我们可以记num[i]为有多少条线段挡住了线段i。每次找到一个入度为0的线段(即没有任何线段能挡住它)并输出,然后,再判断它挡住了什么线段,并把num[那一条线段]的值减1。剩下,只需要判断,某一条线是否挡住了另外一条就可以了:
考虑两条线段,我们可以先把完全不重合的情况去掉。
之后,我们设上面的线段为x,下面的为y,就可以把它们分成两种情况:一种x在y左边,另一种在右边,然后,我们就可以计算x的斜率(在x的线段中,横坐标每增加1时,纵坐标增加的数)。然后,我们就可以计算出y线段上方的点的纵坐标。这样我们就可以O(1)判断了。
【时空复杂度】
时间复杂度:O(n2)。
空间复杂度:O(n2)。
【解题反思】
在做这些题目时,要考虑运用几何巧妙地判断,并且能马上想出算法,完成题目。
【程序】
#include<cstdio>
#include<iostream>
#include<vector>
#include<map>
#include<algorithm>
#include<cmath>
using namespacestd;
struct data{
int x1,y1,x2,y2;
}f[5010];//记录线段
bool id[5010];//是否已被删除
bool bo;
int num[5010];//阻挡线段的个数
int n;
inline boolpd(data x,data y)//y是否挡住x
{
bool
boo1=y.x1>=x.x1 && y.x1<=x.x2,
boo2=y.x2>=x.x1 && y.x2<=x.x2,
boo3=x.x1>=y.x1 && x.x1<=y.x2,
boo4=x.x2>=y.x1 && x.x2<=y.x2;
bo=0;
if(!boo1 && !boo2)
{
if(!boo3 && !boo4) return 0;
else//需要调换过来
{
data t=x;
x=y;
y=t;
boo1=boo3;boo2=boo4;
bo=true;//结果也要相反
}
}
if(x.x1==x.x2)//防止下面除以零的情况,需提前处理
{
if(boo1)
return y.y1>x.y1?0:1;
if(boo2)
return y.y2>x.y1?0:1;
}
if(boo1)
{
doublek=(x.y1-x.y2)*1.0/(x.x1-x.x2)*1.0;//计算斜率
returny.y1>x.y1*1.0+(y.x1-x.x1)*k?0:1;//是否在它上方
}
else if(boo2)
{
doublek=(x.y1-x.y2)*1.0/(x.x1-x.x2)*1.0;//计算斜率
return y.y2>x.y1*1.0+(y.x2-x.x1)*k?0:1;//是否在它上方
}
return 0;
}
int main()
{
freopen("xd.in","r",stdin);
freopen("xd.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d%d%d%d",&f[i].x1,&f[i].y1,&f[i].x2,&f[i].y2);
if(f[i].x1 > f[i].x2)
{
swap(f[i].x1,f[i].x2);
swap(f[i].y1,f[i].y2);
}
}
for(int i=1;i<=n;i++)//预处理刚开始的情况
{
for(int j=1;j<=n;j++)
if(i!=j)
{
bool p=pd(f[i],f[j]);
if(bo) p=p?0:1;
if(p)
{
num[i]++;
}
}
}
//拓扑排序
int j;
for(int i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
if( (!id[j]) && num[j]==0)//找到一个num值为0的线段(入度为0),即下面没有任何线段阻挡
{
break;
}
printf("%d ",j);
id[j]=1;//删除
for(int k=1;k<=n;k++)//把它阻挡的线段的num的个数-1
if(!id[k])
{
bool p=pd(f[k],f[j]);
if(bo) p=p?0:1;
if(p) num[k]--;
}
}
return 0;
}
第6题 方案数(fas)
【题意分析】
有n个人,取红蓝2 种球。限制:
1,每个人最少取一个球,只能取相同颜色的球,第 i个人最多取ai个红球,bi个蓝球。
2,取红球数的人不少于c个。
有q次询问,每次修改某个人的ai,bi限制,输出当前条件下的可能方案数模10007。
【试题难度】
★★★★★
【解题思路】
这题很容易想到递推,设f(i,k)为前i个有k个人选红球的方案数,那么,f(i,k) = f(i-1,k-1)*ai + f(i-1,k)*bi。由于选红球的人数不小于c个,所以,总方案数为∑f(n,i)(c<=i<=n)。
这样的话会超时,然后我们也可以很容易就想到用胜者树来高效维护。这样,就可以在规定时间内解决问题了。
【时空复杂度】
时间复杂度:O(nc2+qlogn*c2)。(时间限制为10秒,不超时)
空间复杂度:O(nlogn*c)。
【解题反思】
1, 要想到用乘法原理来解决问题,在短时间内写出正确的递推式。
2, 学会运用胜者树。
【程序】
#include<cstdio>
#include<iostream>
using namespacestd;
const int MOD =10007;
int f[200010][21];
int a[100010];
int b[100010];
int n,q,c;
inline voidupdate(int x)//暴力修改胜者树
{
for(int i=0;i<=c;i++) f[x][i] = 0;
for(int i=0;i<=c;i++)
for(int j=0;j<=c;j++)
f[x][min(i+j,c)] =(f[x][min(i+j,c)] + ( f[x*2][i] * f[x*2+1][j] )) %MOD;
for(int i=0;i<=c;i++)
f[x][i] %= MOD;
}
inline voidchange(int x)//限制变了,整棵树也要变
{
x+=n;
memset(f[x],0,sizeof(f[x]));
f[x][0] = b[x-n] % MOD;
f[x][1] = a[x-n] % MOD;
for(x/=2;x>0;x/=2)
{
update(x);
}
}
int main()
{
freopen("fas.in","r",stdin);
freopen("fas.out","w",stdout);
int p=0;
scanf("%d%d",&n,&c);
for(int i=0;i<n;i++)scanf("%d",&a[i]);
for(int i=0;i<n;i++)scanf("%d",&b[i]);
for(int i=0;i<n;i++)
{
p=i+n;
f[p][0] = b[i] % MOD;
f[p][1] = a[i] % MOD;
}
for(int i=n-1;i>=1;i--) update(i);//预处理
scanf("%d",&q);
for(;q>0;q--)
{
scanf("%d",&p); p--;
scanf("%d%d",&a[p],&b[p]);
change(p);
printf("%d\n",f[1][c]);//输出根节点
}
return 0;
}