題解的順序按照自己認為的難易順序排列。
K.最澄澈的空與海
本題直接輸出即可,注意輸出引號與反斜杠的格式。
J.千年幻想鄉 ~ History of the Moon
本題是作者被儒略歷坑慘之后用怨念口胡的題 (幻想鄉歷法與生辰八字的轉化) 的極度簡化版。
只要在一開始時按照周期六十預處理每個紀年所互相對應的結果后就可以對每個詢問快速處理。
由於數據比較小,實際上你每一步都從已知的年份模擬下來也能過,std::map
更可以。
當然你要解線性同余方程組得到解析解,我也不攔你(笑)。
下面的解法是預處理解法。
char sys[3][5][10]={{"hi","tsuki","hoshi"},//日月星
{"haru","natsu","aki","fuyu"},//春夏秋冬
{"tsuchi","ka","mizu","ki","kin"}};//土火水木金
char gz[2][12][10]={{"yi","bing","ding","wu","ji","geng","xin","ren","gui","jia"},//乙~甲
{"you","xu","hai","zi","chou","yin","mao","chen","si","wu","wei","shen"}};//酉~申
inline int toSys1(char s[])
{
if(s[0]=='h' && s[1]=='i') return 0;
if(s[0]=='t') return 1;
if(s[0]=='h' && s[1]=='o') return 2;
}
inline int toSys2(char s[])
{
if(s[0]=='h') return 0;
if(s[0]=='n') return 1;
if(s[0]=='a') return 2;
if(s[0]=='f') return 3;
}
inline int toSys3(char s[])
{
if(s[0]=='t') return 0;
if(s[0]=='k' && s[1]=='a') return 1;
if(s[0]=='m') return 2;
if(s[0]=='k' && s[2]==0) return 3;
if(s[0]=='k' && s[2]=='n') return 4;
}
inline int toGz1(char s[])
{
if(s[0]=='y') return 0;
if(s[0]=='b') return 1;
if(s[0]=='d') return 2;
if(s[0]=='w') return 3;
if(s[0]=='j' && s[2]==0) return 4;
if(s[0]=='g' && s[1]=='e') return 5;
if(s[0]=='x') return 6;
if(s[0]=='r') return 7;
if(s[0]=='g' && s[1]=='u') return 8;
if(s[0]=='j' && s[2]=='a') return 9;
}
inline int toGz2(char s[])
{
if(s[0]=='y' && s[1]=='o') return 0;
if(s[0]=='x') return 1;
if(s[0]=='h') return 2;
if(s[0]=='z') return 3;
if(s[0]=='c' && s[2]=='o') return 4;
if(s[0]=='y' && s[1]=='i') return 5;
if(s[0]=='m') return 6;
if(s[0]=='c' && s[2]=='e') return 7;
if(s[0]=='s' && s[1]=='i') return 8;
if(s[0]=='w' && s[1]=='u') return 9;
if(s[0]=='w' && s[1]=='e') return 10;
if(s[0]=='s' && s[1]=='h') return 11;
}
//預處理
inline void init()
{
for(int k=0;k<60;k++)
kg[k%10][k%12]=ksys[k%3][k%4][k%5]=k;
}
//詢問處理
for(int i=1;i<=q;i++)
{
int t;
scanf("%d",&t);
if(t==1)
{
char str1[10],str2[10],str3[10];
scanf("%s%s%s",str1,str2,str3);
int k=ksys[toSys1(str1)][toSys2(str2)][toSys3(str3)];//todo
printf("%s %s\n",gz[0][k%10],gz[1][k%12]);
}
else
{
char str1[10],str2[10];
scanf("%s%s",str1,str2);
int k=kg[toGz1(str1)][toGz2(str2)];//todo
printf("%s %s %s\n",sys[0][k%3],sys[1][k%4],sys[2][k%5]);
}
}
B.53分鍾的藍色大海 ~ Blue Sea for 53 minutes
首先,你需要將字符轉化為高度,以便處理。
然后對於一個視圖來說,某一列的高度的最大值決定了在這樣一列的投影的高度。
所以,我們對於每一行(列)都求出這一(列)的高度最大值,就是側(正)視圖上的高度。
最后以縱向#
的個數代表高度輸出。
inline int Max(int a,int b)
{
return a>b?a:b;
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++) scanf("%s",g[i]+1);
for(int i=1;i<=k;i++)
{
scanf("%s%d",&t,&a);
sample[t[0]]=a;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
mx=Max(mx,sample[g[i][j]]);
fr[j]=Max(fr[j],sample[g[i][j]]);
si[i]=Max(si[i],sample[g[i][j]]);
}
printf("Front View\n");
for(int i=mx;i>=1;i--)
{
for(int j=1;j<=m;j++) putchar(fr[j]>=i?'#':' ');
putchar('\n');
}
putchar('\n');
printf("Side View\n");
for(int i=mx;i>1;i--)
{
for(int j=1;j<=n;j++) putchar(si[j]>=i?'#':' ');
putchar('\n');
}
for(int j=1;j<=n;j++) putchar(si[j]>=1?'#':' ');//to avoid end empty line
return 0;
}
A.廣重36號 ~ Neo Super-Express
本題等價於求一個平均數最大的子矩陣。
因為對於一組數 \(a_i\) 一個數 \(p < \bar{a}_n\),將 \(p\) 加入數列 \(a\) 后數列的平均值 \(\bar{a}_{n+1}=\frac{p+n* \bar{a}_n}{n+1}=\frac{(n+1)* \bar{a}_n+p-\bar{a}_n}{n+1}=\bar{a}_n+\frac{p-\bar{a}_n}{n+1}<\bar{a}_n\)。
所以我們知道,選入要選的矩陣中的數偏小,總共的平均值也會偏小,類似於被“沖淡”的作用。
所以我們只需要輸出整個矩陣中元素的最大值即可。
D.彼岸歸航 ~ Riverside View
我們容易知道,一個矩形是某個大矩形的子矩形,當且僅當大矩形的編碼為小矩形編碼的前綴。
所以我們可以對字符串前綴排序,如果后面的矩形編碼有前面矩形的作為其前綴的時候,那么說明這個矩形被覆蓋到了,不將它統計入答案,否則統計進答案。
因為已經經過前綴排序,所以大矩陣肯定在子矩陣前面。於是考慮順序遍歷排完序的字符串數組,存儲一個可能作為前綴的編碼。如果該編碼以存儲編碼作為前綴,那么略過,否則計入答案,更新存儲的編碼(因為它可能作為后面某編碼的前綴)。
int n;
double sa,ans;
const double all=(double)(1<<18);
struct NHash
{
int len;
char code[24];
}sc[N];
bool cmp1(NHash a,NHash b)//前綴排序
{
int len=min(a.len,b.len);
for(int i=0;i<len;i++) if(a.code[i]!=b.code[i]) return a.code[i]<b.code[i];
return a.len<b.len;
}
bool cmp2(NHash a,NHash b)//判斷a是否不為b的前綴
{
//printf("cmp %s %s\n",a.code,b.code);
if(a.len>b.len) return true;
else
{
for(int i=0;i<a.len;i++)
{
//printf("\t%d %c %c\n",i,a.code[i],b.code[i]);
if(a.code[i]!=b.code[i])
return true;
}
}
return false;
}
inline void Solve()
{
#ifdef DEBUG
printf("Debuging...\n");
#endif
scanf("%d%lf",&n,&sa);
sa*=sa;
for(int i=1;i<=n;i++)
{
scanf("%s",sc[i].code);
sc[i].len=strlen(sc[i].code);
}
sort(sc+1,sc+n+1,cmp1);
//n=unique(sc+1,sc+n+1)-sc;
NHash now={0,""};
for(int i=1;i<=n;i++)
{
if(i==1 || cmp2(now,sc[i])) ans+=1ll<<(19-sc[i].len),now=sc[i];//,printf("add %d\n %s\n",sc[i].len-1,sc[i].code)
}
//printf("%f\n",ans);
printf("%.3f\n",ans/all*sa);
return ;
}
因為和前綴有關系,所以你寫一顆Trie然后遍歷也是對的。
但是由於個人對題目復雜度情況的疏忽,導致了二維差分與直接以編碼為Hash值查重的解法也能夠通過本題。
現在出題人很后悔(
I.Locked Girl ~ 少女密室
我們對代碼文本進行編輯,只允許刪除和添加,那么所求最小值與最長公共子序列有關,第一個代碼段中未出現最長公共子序列部分即被刪除,第二個代碼段中未出現最長公共子序列部分即為添加部分。
最長公共子序列是指兩個字符串里面連續不一定相鄰的最長的公共字符,
假設我們用\(dp[i,j]\)表示 A 和 B 的LCS的長度(直接保存最長公共子序列的中間結果不現實,需要先借助LCS的長度),那么存在以下遞推式
H.回憶京都 ~ Retrospective Kyoto
通過對小數據的觀察分析發現,我們只需要構造圖的一棵生成樹。先從最底下的邊開始把便士放到底部的葉子節點上(從它的父節點移動到該節點),如果某個節點已經只有一個空點與其相連,那么它也就成為了“葉子節點”進入決策范圍中。不斷重復這個過程,直到只剩下根節點作為空節點。
整個過程可以用dfs實現。
void dfs(int x)
{
vi[x]=1;
for(int i=H[x];i;i=K[i])
{
int y=V[i];
if(vi[y]) continue;
fm[++cnt]=x,to[cnt]=y;
dfs(y);
}
}
//...
dfs(1);
if(cnt!=n-1) return printf("Impossible!\n");
else
{
for(int i=cnt;i>=1;--i)
printf("%d -> %d\n",fm[i],to[i]);
}
F.宇佐大人的白旗 ~ Most Famous Hero
本題相當於在一個有正邊權和整數點權的DAG上找到一條路徑和該路徑上的一些點,使得總權值最小。直接Topo Sort后在DAG上進行dp即可。
//c: 單個站點的凈收益
//q:topo sort的隊列
inline ll Topo()
{
d[s]=0;
head=1,tail=0;
for(int x=1;x<=n;x++)
if(!deg[x]) q[++tail]=x;
for(;head<=tail;)
{
int x=q[head++];
vi[x]=1;
if(x==t) return d[x]+c[x];
d[x]+=c[x];
for(int i=H[x];i;i=K[i])
{
int y=V[i];
if(vi[y]) continue;
--deg[y];
if(!deg[y]) q[++tail]=y;
d[y]=min(d[y],d[x]+E[i]);
}
}
return 114514;
}
inline void Solve()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",c+i);
for(int i=1;i<=n;i++){int p; scanf("%d",&p); c[i]-=p;}
for(int i=1;i<=m;i++)
{
int u,v,w; scanf("%d%d%d",&u,&v,&w);
Add(u,v,w),++deg[v];
}
scanf("%d%d",&s,&t);
//for(int i=1;i<=n;i++) printf("%d ",c[i]); putchar('\n');
for(int i=1;i<=n;i++) if(i!=s && i!=t) c[i]=min(c[i],0);
//for(int i=1;i<=n;i++) printf("%d ",c[i]); putchar('\n');
memset(d,0x3f,sizeof(d));
printf("%lld\n",Topo());
//for(int i=1;i<=n;i++) printf("%d-%lld\n",i,d[i]);
return ;
}
C.竹取飛翔 ~ Lunatic Princess
由高中數學知識可以知道,一個大小為 \(k\) 的可重集 \(S\) 的全部子集的元素和之和應該等於 \(2^{k-1} \sum{S_i}\)(證明的話考慮每個元素出現在那幾個子集里即可)。集合的合並考慮用並查集維護,注意合並時應維護集合的大小與元素之和。
//並查集
int fa[N],rnk[N];
ll sum[N];
inline void init(){for(int i=1;i<=n;i++) fa[i]=i,rnk[i]=1;}
int Find(int x){return fa[x]==x?x:fa[x]=Find(fa[x]);}
inline void Union(int x,int y)
{
x=Find(x),y=Find(y);
if(x==y) return;
if(rnk[x]<rnk[y]) swap(x,y);
fa[y]=x,rnk[x]+=rnk[y],sum[x]=(sum[x]+sum[y])%p;
}
//in every query
int op=read();
if(op==1)
{
int x=read(),y=read();
Union(x,y);
}
else if(op==2)
{
int x=read(),k=read(); x=Find(x);
sum[x]=(sum[x]+p+k)%p;
}
else
{
int x=read(); x=Find(x);
printf("%lld\n",(qpower(2,rnk[x]-1)*sum[x])%p);
}
D.彼岸歸航 ~ Riverside View
設字符串長度為 \(n\)。
Solution 1
暴力修改(一堆人這么做),復雜度 \(O(nm)\),可以獲得Wrong Answer的好成績。
Solution 2
本題相當於區間推平單點查詢操作(一開始區間修改,最后 \(n\) 次單點查詢)。考慮用線段樹維護,區間修改區間內的字符格式,查詢每個字符。復雜度 \(O((n+m)\log n)\),可以通過此題。
inline void push(int rt)
{
caps[rt]=max(caps[rt<<1],caps[rt<<1|1]);
}
void build(int l,int r,int rt)
{
if(l==r)
{
if(s[l]>='A'&&s[l]<='Z')
{
caps[rt]=2;
s[l]+=32;
}
else caps[rt]=1;
return;
}
int mid=(l+r)>>1;
build(lson);
build(rson);
push(rt);
}
inline void pushdown(int rt,int len)
{
if(!add[rt]) return;
add[rt<<1]=add[rt];
add[rt<<1|1]=add[rt];
caps[rt<<1]=add[rt];
caps[rt<<1|1]=add[rt];
add[rt]=0;
}
void modify(int L,int R,int v,int l,int r,int rt)
{
if(L<=l&&r<=R)
{
add[rt]=v;
caps[rt]=v;
return;
}
pushdown(rt,r-l+1);
int mid=(l+r)>>1;
if(L<=mid) modify(L,R,v,lson);
if(R>mid) modify(L,R,v,rson);
push(rt);
}
int query(int L,int R,int l,int r,int rt)
{
if(L<=l&&r<=R)
return caps[rt];
pushdown(rt,r-l+1);
int mid=(l+r)>>1;
int ans=-INF;
if(L<=mid) ans=max(ans,query(L,R,lson));
if(R>mid) ans=max(ans,query(L,R,rson));
return ans;
}
int main()
{
//freopen("data16.in","r",stdin);
//freopen("data16!.out","w",stdout);
int fm,a,b;
scanf("%d",&m);
scanf("%*c%*c%[^\n]",s+1);
//Test:printf("%s",s+1);
n=strlen(s+1);
if((s[1]>='A'&&s[1]<='Z')||(s[1]>='a'&&s[1]<='z')) fir[1]=1;
for(register int i=1;i<=n;i++)
if(s[i-1]==' '&&((s[i]>='A'&&s[i]<='Z')||(s[i]>='a'&&s[i]<='z')))
fir[i]=1;
build(1,n,1);
while(m--)
{
fm=Read();a=Read();b=Read();
modify(a,b,fm,1,n,1);
}
for(register int i=1;i<=n;i++)
{
switch(query(i,i,1,n,1))
{
case 1:putchar(s[i]);break;
case 2:putchar(s[i]==32?32:s[i]-32);break;
case 3:putchar(fir[i]?(s[i]-32):s[i]);
}
}
return 0;
}
Solution 3
因為到了最后才輸出結果,所以考慮離線算法。因為區間推平的覆蓋性,所以每一個點都只考慮最后一次修改,對修改指令倒序處理。然后考慮在線段樹中,每個節點保存一個是否修改過的tag。pushup時就看兩個子節點是否修改過,然后之后只修改從未修改過的節點。
因為這種做法排除了重復修改的冗余操作,最后復雜度等價於n次線段樹單點修改,即 \(O(n\log n)\)。
inline void toup(char& ch){if('a'<=ch&&ch<='z') ch-=32;}
inline void tolow(char& ch){if('A'<=ch&&ch<='Z') ch+=32;}
struct SegmentTree
{
int l,r;
int c;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define c(x) tree[x].c
}tree[4*N];
#define lc (p<<1)
#define rc (p<<1|1)
void build(int p,int l,int r)
{
l(p)=l,r(p)=r;
if(l==r) return;
int mid=(l+r)/2;
build(lc,l,mid);
build(rc,mid+1,r);
}
inline void modify(int k,int q)
{
if(q==1) tolow(str[k]);
if(q==2) toup(str[k]);
if(q==3) tag[k]?toup(str[k]):tolow(str[k]);
}
void update(int p,int q,int l,int r)
{
if(l(p)==r(p)){modify(l(p),q); c(p)=1; return;}
int mid=(l(p)+r(p))/2;
if(l<=mid && !c(lc)) update(lc,q,l,r);
if(mid<r && !c(rc)) update(rc,q,l,r);
c(p)=c(lc)&c(rc);
}
inline void Solve()
{
#ifdef DEBUG
printf("Debuging...\n");
#endif
scanf("%d",&m); cin.getline(tmp,10);
tag[1]=1;
for(char ch=getchar();ch!='\n';ch=getchar())
{
str[++len]=ch;
if(ch==' ') tag[len+1]=1;
}
//printf("%d\n",len);
//printf("%s\n",str+1);
for(int i=m;i>=1;--i)
q[i]=read(),l[i]=read(),r[i]=read();
build(1,1,len);
for(int i=1;i<=m;++i)
update(1,q[i],l[i],r[i]); //printf("%d %d %d %d\n",i,q[i],l[i],r[i]),
printf("%s\n",str+1);
return ;
}
E.青木原的傳說 ~ Forbidden Forest
觀察數據,我們容易發現,一個結點能夠延長的鏈的數量,只與子結點和父結點的權值有關。
而我們也容易知道,能夠讓一條已有鏈延伸得更長或者將兩條鏈盡可能連起來,就有可能使答案更小。
故考慮貪心。
為了讓鏈的數量盡可能的少,對於某一個結點的一個單位的權值,我們應該盡可能的將其子樹上的鏈通過這個結點匹配起來(使得鏈的數量減少一條),其次才是將這個結點加入到某一個子樹的鏈上(鏈的數量不變),最后才是以這點為起點構造新的鏈(使得鏈的數量增加一條)。
Solution 1:
我們可以用 \(a_x\) 表示 \(x\) 點未匹配的權值。
對於每個結點 \(u\),用大根堆維護其子結點 \(v\) 的 \(a_v\)。
如果堆內還有大於等於二個結點,那么每次取出權值最大與次大的子結點,使其與父結點匹配為一條鏈,然后把匹配的鏈消去(父結點,兩個子結點權值各減一),計入答案,即將答案減去一(因為原來的兩條鏈合並成了一條鏈),未匹配完的子結點放回堆中。
每次取出權值最大與次大的子結點,是因為這樣能保證盡可能多的子樹上鏈能夠連成一條鏈。
如果只剩或只有一個子點 \(v\),那么應該有 \(a_v\) 條鏈是從下方傳上來的,從這里開始的鏈的數量為 \(a_u-a_v\),加入答案。
若點本身為葉子結點或全部結點恰好匹配完,說明有 \(a_u\) 條鏈應從這里開始,加入答案。
最終復雜度為 \(O(an \log n)\),可以通過此題。
inline void dfs(int u,int fa){
vis[u]=1;
priority_queue<int,vector<int>,less<int> > q;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;if(v==fa) continue;
dfs(v,u);
if(a[v]) q.push(a[v]);
}
while(a[u]&&(!q.empty())){
int x=q.top();q.pop();
if(q.empty()){
q.push(x);
break;
}
else{
int y=q.top();q.pop();
x--;y--;a[u]--;ans--;
if(x) q.push(x);
if(y) q.push(y);
}
}
if(a[u]&&(!q.empty())){
int x=q.top();q.pop();
if(a[u]>x){
ans+=a[u]-x;
}
}
else if(a[u]&&q.empty()){
ans+=a[u];
}
}
//ans即為所求答案
for(int i=1;i<=n;i++){
if(!vis[i]) dfs(i,i);
}
printf("%d\n",ans);
Solution 2:
(感謝萬指導提供此解法)
我們發現我們把大量的時間都花在了匹配兩條子樹上的鏈,那么有優化的余地嗎?
我們考慮這樣一個貪心:先把一個結點上的所有子結點權值的和 \(sum\) 以及最大值 \(maxv\) 統計出來。
如果 \(2\cdot maxv\geq sum\)(即權值最大的子結點要大於其他所有子結點之和)時,可以匹配的鏈的組數為 \(sum-maxv\)(其他子結點的和),留下不能匹配的鏈的數量為 \(2\cdot maxv-sum\) (最大權值減去其他子結點的和)。
否則,可以匹配的鏈的組數為 \(\lfloor\frac{1}{2}sum\rfloor\)(把所有權值分成兩半),留下不能匹配的鏈的數量為 \(sum\mod 2\) (分成兩半余下的結點,如果 \(sum\) 為偶數則完全匹配)。
粗略證明:
對於 \(2\cdot maxv\geq sum\) 的情形,我們只需要把其他子結點上的權值同有最大權值的子結點上的值匹配即可。
而對於 \(2\cdot maxv < sum\) 的情形,考慮將所有的點一一排序,然后從小到大得把權值平均分配到剩余結點進行匹配,使得每一個結點的權值在處理后盡可能等於平均值。因為最大的權值不會比其他的權值之和要大,而平攤的過程中顯然最大的權值也不會比其他的權值之和要大(因為幾乎是同時減小),所以最后按照平攤的結果一定會是剛好匹配完或者只剩一個(取決於總權值和的奇偶性),而匹配出的鏈的組數顯然是 \(\lfloor\frac{1}{2}sum\rfloor\)。
一開始我們可以假設所有的邊都沒有連起來,鏈的數量就是權值的總和。
通過dfs,我們可以將邊自下而上的連起來,使鏈因連起來而減少。連邊的原則就是按照一開始的順序及上面貪心求出的可以匹配的鏈的組數(可以減去兩條鏈,一條本結點,一條因兩條子鏈連起來而減去)與留下不能匹配的鏈的數量(可以減去一條鏈,來自本結點),當然你得保證這個結點有足夠的權值進行匹配。
因為 \(maxv\) 和 \(sum\) 可以在dfs遍歷子結點時順便求出,所以該方法的復雜度為 \(O(n)\)。
(心心念念的 \(O(n)\) 解法啊())
int vi[N];
ll a[N],d[N],ans=0;//d[u]:u點除去已匹配權值的權值
//dfs
void dfs(int x,int fa)
{
vi[x]=1;
ll sum=0,maxv=0,l1=0,l2=0;
for(int i=H[x];i;i=K[i])
{
int y=V[i];
if(y==fa) continue;
dfs(y,x);
sum+=d[y],maxv=max(maxv,d[y]);
}
//l1-留下不能匹配的鏈的數量,l2-可以匹配的鏈的組數
if(2*maxv>=sum) l2=sum-maxv,l1=maxv-l2;
else l2=sum>>1,l1=sum&1;
//第一種:結點不足以匹配全部子鏈
if(l2>a[x])
d[x]=0,ans-=2*a[x];
//第二種:結點足以匹配全部子鏈,但不足以延長剩余子鏈
else if(l1+l2>a[x])
d[x]-=l2,ans-=2*l2+d[x];
//第三種:結點足以匹配全部子鏈,且足以延長剩余子鏈(葉節點屬於這種情況)
else
d[x]-=l2,ans-=2*l2+l1;
}
//in main
n=read(),m=read();
//因為鏈還未配對起來,所以ans的初值為權值和,d的初值為a
for(int i=1;i<=n;i++) d[i]=a[i]=read(),ans+=a[i];
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
Add(u,v),Add(v,u);
}
for(int x=1;x<=n;x++) if(!vi[x]) dfs(x,0);
printf("%lld\n",ans);