并不对劲的noip2018
day1
road
题意
有n(\(n \leq 10^5\))个数\(a_1,a_2,...,a_n\)排成一排,每次可以选择一段大于零的数减一,问最少几次把所有数减为0。
题解
先想到一个简单的策略:当处理到区间\([l,r]\)时,找出其中的最小值出现的位置\(k\),对这个区间进行\(a_k\)次区间减一,再分别处理\([l,k-1]\)和\([k+1,r]\)。
但是区间\([l,r]\)要是有很多个最小值,出现位置为\(k_1,k_2,\)...\(,k_p\),该怎么办?想必是没什么影响的。对这个区间进行最小值次区间减一后,\(k_1,k_2,\)...\(,k_p\)上的数都变成0,选其中的哪一个作为断点都没什么区别。
要想维护区间最值,用st表就行了。
代码
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<ctime>
#include<iomanip>
#include<iostream>
#include<map>
#include<stack>
#include<vector>
#include<queue>
#define rep(i,x,y) for(int i=(x);i<=(y);++i)
#define dwn(i,x,y) for(int i=(x);i>=(y);--i)
#define maxn 100010
using namespace std;
int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
void write(int x)
{
if(x==0){putchar('0'),putchar('\n');return;}
if(x<0){putchar('-'),x=-x;}
char ch[20];int f=0;
while(x)ch[++f]=x%10+'0',x/=10;
while(f)putchar(ch[f--]);
putchar('\n');
}
int n,d[maxn],st[20][maxn],lg[maxn],ans=0;
int MIN(int l,int r)
{
int len=r-l+1;
return d[st[lg[len]][l]]<=d[st[lg[len]][r-(1<<lg[len])+1]]?st[lg[len]][l]:st[lg[len]][r-(1<<lg[len])+1];
}
void work(int l,int r,int lst)
{
int minx=MIN(l,r);
ans+=(d[minx]-lst);
if(l<=minx-1)work(l,minx-1,d[minx]);
if(minx+1<=r)work(minx+1,r,d[minx]);
return;
}
int main()
{
n=read();
rep(i,1,n)d[i]=read(),st[0][i]=i;
lg[0]=-1;
rep(i,1,n)lg[i]=lg[i>>1]+1;
rep(k,1,lg[n])for(int i=1;i+(1<<k)-1<=n;i++)
st[k][i]=d[st[k-1][i]]<=d[st[k-1][i+(1<<(k-1))]]?st[k-1][i]:st[k-1][i+(1<<(k-1))];
work(1,n,0);
write(ans);
return 0;
}
money
题意
有\(n\)(\(n\leq 100\))个数\(a_1,a_2,\)...\(,a_n\)(\(\forall i\in[1,n],a_i\leq25000\))。
定义“凑出”是:当存在一个\(n\)个数的数列\(t_1,t_2,\)...\(,t_n\)使\(\sum_{i=1}^n a_i*t_i \space= k\)时,k能被这n个数凑出。
求最小的数\(m\),使存在m个数\(b_1,\)...\(,b_m\),这些数能凑出\(a_1,\)...\(,a_n\)能凑出的所有数,凑不出\(a_1,\)...\(,a_n\)凑不出的所有数。
共\(T\)(\(T\leq20\))组输入。
题解
把这\(n\)个数中所有能被\(n\)个数中的其它数凑出的删掉。
正确性:假设数\(x\)能被\(n\)个数中的其它数凑出,那么\(x\)能凑出的数一定能被凑出\(x\)的数凑出,删掉\(x\)后,原来能凑出的数还能被凑出。
最优性:那么这个可以用反证法来证明,假设存在一种删去了p个数,又添加了q(q<p)个数的方法比只删数更优。为了让方法更优,添加的数一定不能被未删除的数凑出。但是,这样添加的数就是原来凑不出的数,现在能凑出了,与题意不符。所以添加数不会使答案更优,只删掉能被凑出的数就行了。
需要判断每个数能否被凑出,这是个完全背包问题。
代码
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<ctime>
#include<iomanip>
#include<iostream>
#include<map>
#include<stack>
#include<vector>
#include<queue>
#define rep(i,x,y) for(int i=(x);i<=(y);++i)
#define dwn(i,x,y) for(int i=(x);i>=(y);--i)
#define maxk 25010
#define maxn 110
using namespace std;
int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
void write(int x)
{
if(x==0){putchar('0'),putchar('\n');return;}
if(x<0){putchar('-'),x=-x;}
char ch[20];int f=0;
while(x)ch[++f]=x%10+'0',x/=10;
while(f)putchar(ch[f--]);
putchar('\n');
}
int yes[maxk],t,maxpos,a[maxn],n,ans,maxlim;
int main()
{
t=read();
while(t--)
{
n=read(),ans=n;maxlim=maxpos=0;
rep(i,1,n)a[i]=read(),maxlim=max(maxlim,a[i]);
rep(i,1,maxlim)yes[i]=0;yes[0]=1;
sort(a+1,a+n+1);
rep(i,1,n)
{
if(yes[a[i]]){ans--;continue;}
rep(j,0,maxpos)
{
if(j+a[i]>maxlim)break;
if(yes[j])yes[j+a[i]]=1,maxpos=max(maxpos,j+a[i]);
}
}
write(ans);
}
return 0;
}
track
题意
有一棵有\(n\)(\(n\leq5*10^4\))的树,有边权。
要把这棵树拆成\(m\)条链,链之间可以有公共点,但不能有公共边。
求一种拆分方案,使边权和最小的链边权和最大。
题解
“最大值最小”显然是需要二分的,然后就是判断这棵树能否拆成不少于m条边权和大于等于k的链。
这样问题就转换成:对于每个点,选若干对儿子,使得每一对中,两个儿子向下连的直链长度,加上该点到两个儿子的边大于等于\(k\),在剩下儿子中选择向下的直链长度加该点到这个儿子的边长度最长的作为该点向下的直链。需要找出一种方法使选出的儿子对的个数尽可能多,在此基础上,该点向下的直链长度尽可能长。
\(l_i\)表示在处理完\(i\)的子树后,点\(i\)向下连的链最长有多长。
在计算完点\(u\)的所有儿子\(v_i\)的\(l_{v_i}\)后,将所有儿子按\(l_{v_i}+i到这个儿子的距离\)排序,第\(j\)个记作\(a_j\)。
这样问题就变成在单调不降数列\(a_1,\)...\(,a_p\)中,选出若干对数,同一个数只能被选一次,使每一对的和大于等于\(k\),选出的数对的个数尽可能多,在此基础上,剩下的数中最大的数尽可能大。
如果只用考虑数对个数最多,就可以这样:找出位置\(q\)使\(k\leq a_q+a_{q+1}\)且\(k\geq a_{q-1}+a_q\),那么对于小于\(a_q\)的数,只有在大于\(a_q\)的数中才能找到与它相加大于\(k\)的。那么就可以从\(a_{q-1}\)到\(a_1\),判断是否存在\(i\)使\(a_i\)未被选且\(当前数+a_i\geq k\),如果有,就取符合条件的最小的\(a_i\)。判断完所有小于\(a_q\)的数后,未被选的大于等于\(a_q\)的数直接两两配对就行了。以上过程的时间复杂度是\(O(n)\)的,可以维护一个指针\(i\),初始位置为\(q\),每当\(当前数+a_i\leq k\)时,右移\(i\),因为随着当前数减小,符合条件的与它配对的数是不断增大的。
那要是要考虑在选出数对个数最多的基础上,剩下的数的最大值尽可能大呢?看上去没有简单的贪心策略。先有个暴力的想法:枚举最后剩下的数中的最大值,求删掉枚举的数后的数列中最多能选出多少对。会发现删掉较大的数后的答案肯定不会比删掉较小的数的答案更大,也就是有单调性。那么只要先二分出剩下的数的最大值,再贪心求出删掉该数后最多能凑出几对就行了。
代码
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<ctime>
#include<iomanip>
#include<iostream>
#include<map>
#include<stack>
#include<vector>
#include<queue>
#define rep(i,x,y) for(int i=(x);i<=(y);++i)
#define dwn(i,x,y) for(int i=(x);i>=(y);--i)
#define maxn 50010
#define maxm 100010
using namespace std;
int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
void write(int x)
{
if(x==0){putchar('0'),putchar('\n');return;}
if(x<0){putchar('-'),x=-x;}
char ch[20];int f=0;
while(x)ch[++f]=x%10+'0',x/=10;
while(f)putchar(ch[f--]);
putchar('\n');
}
int num,ans,mid,n,m,fa[maxn],fir[maxn],nxt[maxm],v[maxm],w[maxm],sons;
int tofa[maxn],son[maxn],tmp[maxn],tn,mk[maxn],len[maxn],L=10001,R,cnt,tmpnum;
void ade(int u1,int v1,int w1){v[cnt]=v1,w[cnt]=w1,nxt[cnt]=fir[u1],fir[u1]=cnt++;}
int getx(int u){return len[u]+tofa[u];}
bool cmp(int x,int y){return getx(x)<getx(y);}
void check(int lim)
{
tmpnum=tn=0;int pos=0;
rep(i,1,sons)if(i!=lim)tmp[++tn]=son[i];
rep(i,1,tn-1)if(getx(tmp[i])+getx(tmp[i+1])>=mid){pos=i;break;}
if(pos==0){return;}
else
{
rep(i,1,tn)mk[i]=0;
int b=pos+1,c=0;
dwn(a,pos,1)
{
if(b>tn)break;
while(b<tn&&getx(tmp[a])+getx(tmp[b])<mid)b++;
if(getx(tmp[a])+getx(tmp[b])>=mid)mk[a]=mk[b]=1,b++,tmpnum++;
else break;
}
rep(i,pos+1,tn)if(!mk[i])c++;
tmpnum+=c/2;
}
}
void getans(int u)
{
for(int k=fir[u];k!=-1;k=nxt[k])if(v[k]!=fa[u]){getans(v[k]);}
sons=0;
for(int k=fir[u];k!=-1;k=nxt[k])if(v[k]!=fa[u]){if(getx(v[k])>=mid)num++;else son[++sons]=v[k];}
if(sons==0){len[u]=0;return;}
sort(son+1,son+sons+1,cmp);
int pos=0;
rep(i,1,sons-1)if(getx(son[i])+getx(son[i+1])>=mid){pos=i;break;}
if(pos==0){len[u]=getx(son[sons]);return;}
else
{
int l=1,r=sons,ansnum=0,anslen=0;
check(0);ansnum=tmpnum;
while (l<=r)
{
int mi=(l+r)>>1;check(mi);
if(tmpnum>ansnum||(tmpnum==ansnum&&getx(son[mi])>anslen))ansnum=tmpnum,anslen=getx(son[mi]),l=mi+1;
else r=mi-1;
}
num+=ansnum,len[u]=anslen;
}
return;
}
int check()
{
num=0;
rep(i,1,n)len[i]=0;
getans(1);
if(num>=m)return 1;
else return 0;
}
void getfa(int u)
{
for(int k=fir[u];k!=-1;k=nxt[k])
if(v[k]!=fa[u])tofa[v[k]]=w[k],fa[v[k]]=u,getfa(v[k]);
}
int main()
{
n=read(),m=read();
memset(fir,-1,sizeof(fir));
rep(i,1,n-1){int x=read(),y=read(),z=read();ade(x,y,z),ade(y,x,z);L=min(L,z),R+=z;}
getfa(1);
while(L<=R)
{
mid=((L+R)>>1);int f=check();
if(f)ans=max(mid,ans),L=mid+1;
else R=mid-1;
}
write(ans);
fclose(stdout);
return 0;
}
day2
travel
题意
有一棵有\(n\)(\(n\leq 5000\))个点的树或基环外向树。
每个点只能走一次,求字典序最小的DFS序。
题解
如果是树,就贪心地先走编号更小的儿子。
有环的话,就枚举删掉环上的哪条边,变成树。
需要注意的是,要在枚举删边之前将每个点的儿子排序。要不然\(O(n^2 log n)\)会被卡。
代码
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<ctime>
#include<iomanip>
#include<iostream>
#include<map>
#include<queue>
#include<set>
#include<stack>
#include<vector>
#define rep(i,x,y) for(register int i=(x);i<=(y);++i)
#define dwn(i,x,y) for(register int i=(x);i>=(y);--i)
#define maxn 5010
using namespace std;
int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
void write(int x)
{
if(x==0){putchar('0'),putchar(' ');return;}
int f=0;char ch[20];
if(x<0)putchar('-'),x=-x;
while(x)ch[++f]=x%10+'0',x/=10;
while(f)putchar(ch[f--]);
putchar(' ');
return;
}
int to[maxn][maxn],tl[maxn],qx[maxn],qy[maxn],num,n,m,nox,noy,vis[maxn],ans[maxn],tmp[maxn],tmpn,cntq,yes;
void work(int u)
{
vis[u]=1,tmp[++tmpn]=u;
rep(k,1,tl[u])if(!vis[to[u][k]]&&(u!=nox||to[u][k]!=noy)&&(u!=noy||to[u][k]!=nox))work(to[u][k]);
}
void getcir(int u,int fa)
{
vis[u]=1;
rep(k,1,tl[u])
{
if(to[u][k]!=fa)
{
if(vis[to[u][k]])
{
yes=1;
qx[++cntq]=u,qy[cntq]=to[u][k];
return;
}
else
{
getcir(to[u][k],u);
if(yes==1)
{
if(u==qy[1])yes=-1;
qx[++cntq]=u,qy[cntq]=to[u][k];
return;
}
if(yes)return;
}
}
}
}
void upd()
{
if(ans[1]!=0)
{
rep(i,1,n)
{
if(tmp[i]<ans[i])break;
if(tmp[i]>ans[i])return;
}
}
rep(i,1,n)ans[i]=tmp[i];
return;
}
int main()
{
n=read(),m=read();
rep(i,1,m){int x=read(),y=read();to[x][++tl[x]]=y,to[y][++tl[y]]=x;}
rep(i,1,n)sort(to[i]+1,to[i]+tl[i]+1);
if(n-1==m)
{
work(1);
rep(i,1,n)write(tmp[i]);
return 0;
}
getcir(1,0);
rep(i,1,cntq)
{
nox=qx[i],noy=qy[i];tmpn=0;
rep(i,1,n)vis[i]=0;
work(1);
upd();
}
rep(i,1,n)write(ans[i]);
return 0;
}
game
自闭了,不写。
defence
自闭了,不写。
总结
爆零就完事儿了
我记得有一次我接了个任务:砍五只大野猪
我想:我都是个上位猎人了,单挑火龙一家、两只雷狼都不带掉血的,五只大野猪算什么?
于是拿着古结云大剑,啥都没穿,啥都没拿就去了
结果被秋名山猪神安排得明明白白的,虽然完成了,但是过程不太愉快
可能有一些方面是因为轻敌,但主要还是走位不够灵活、预判不够精准及时、对地形的运用不够好导致的
活用道具、合理配置装备,确实是比较重要的
但要想在技术上更进一步,就必须尝试脱离这些东西,只靠自己的脑子和手战斗
在这种情况下能够通关后,也该尝试着在通关速度上更进一步,或者用不常使用的武器(大剑、片手以外的所有武器。。。)通关
\(\color{white}{\text{事实上,这个任务上位猎人拿脚玩都没问题……所以,你知道我只是在比喻,对吧?}}\)
我什么时候才能成为能裸装零针迅龙的大剑使呢?