【清北學堂2018-刷題衝刺】Contest 8
阿新 • • 發佈:2018-11-12
Task 1:關聯點
【問題描述】
⼆叉樹是⼀種常用的資料結構,⼀個⼆叉樹或者為空,或者由根節點、左⼦樹、右⼦樹構成,其中左⼦樹和右⼦樹都是⼆叉樹. 每個節點a 可以儲存⼀個值val.
顯然,如果⼀個點a 的左⼦樹或右⼦樹內有⼀個點b,那麼存在唯⼀的路徑從a 出發,每次往左⼦樹或右⼦樹⾛,經過⼀系列節點訪問到b. 我們把從a 到b 經過除a 以外的節點數稱為節點a 到節點b 的距離.
對於⼀個點a,定義:
若點\(b\) 在\(a\) 的左⼦樹中,且\(a\) 到\(b\) 的距離為\(v[b]\),則稱\(b\) 為\(a\) 的左關聯點;
若點\(b\) 在\(a\) 的右⼦樹中,且\(a\) 到\(b\) 的距離為\(v[b]\),則稱\(b\) 為\(a\) 的右關聯點.
給定⼀個共有\(n\) 個節點的⼆叉樹,所有節點編號為\(1, 2, ...,n,\)其中1 為根節點. 給出每個節點\(a\) 儲存的值\(v[a]\),請輸出每個節點的左關聯點個數和右關聯點個數.
【輸入格式】
輸⼊⽂件名為\(node.in\)
第⼀⾏⼀個正整數\(n\),表示⼆叉樹的總節點數.
第⼆⾏\(n\) 個由空格分隔的正整數\(v1,v2,..., vn\)第\(i\) 個數\(vi\) 表示節點\(i\) 儲存的值.
接下來\(n\) ⾏,第\(i\) ⾏為兩個由空格分隔的整數,分別表示編號為\(i\) 的左⼦樹的根節點(若左⼦樹為空則為0)和右⼦樹的根節點(若右⼦樹為空則為0).
【輸出格式】
輸出⽂件名為\(node.out\)
輸出\(n\) ⾏,第\(i\) ⾏為兩個整數,分別表示點\(i\) 的左關聯點個數和右關聯點個數.
node.in | node.out |
---|---|
5 | 2 0 |
2 1 3 2 1 | 0 1 |
2 3 | 0 0 |
4 5 | 0 0 |
0 0 | 0 0 |
0 0 | |
0 0 |
【樣例說明】
節點1 的左關聯點有2 個:2 和4,沒有右關聯點.
節點2 沒有左關聯點,右關聯點有1 個:5.
除此之外,其它節點沒有關聯點.
【資料規模與約定】
- 對於30% 的資料,\(n <= 3\).
- 對於60% 的資料,\(n <= 500\).
- 對於100% 的資料,\(n <= 200000,1 <= vi <= 200000\),根節點1 到任意節點的距離不超過100000.
題目本身不難理解,這裡給出四種寫法思路:
- 暴力O(n^2)(60pts),直接模擬某個點\(u\)向上找\(val[ u ]\)的節點,記錄答案
- 倍增O(nlogn),暴力基礎上的優化,以更快的速度向上尋找節點記錄答案
- 堆O(nlogn),在向下搜尋的時候把該節點對應的父節點扔進堆裡維護,按dfn排序,到達時退棧即可
- 棧O(n),由於搜尋本身就是入棧退棧的過程,只需要搜尋同時維護一個棧就可以O(1)找到答案。
本蒻的程式碼裡寫的是第三種。後三種程式碼都是可以AC的,但棧的寫法才是最優秀的。比較簡單不做贅述。
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define MAXN 200010
using namespace std;
int n,arr[MAXN],ans_1[MAXN],ans_2[MAXN],dfn[MAXN];
struct Node{int ls,rs,fa;}node[MAXN];
struct Rec{
int deep;
bool operator<(const Rec &rhs)const{
return deep<rhs.deep;//get deeper first
}
};
priority_queue<Rec>que;
void pre(int u,int deep){
dfn[u]=deep;
if(node[u].ls!=0)pre(node[u].ls,deep+1);
if(node[u].rs!=0)pre(node[u].rs,deep+1);
}
void dfs(int u){
if(node[u].ls!=0)dfs(node[u].ls);
if(node[u].rs!=0)dfs(node[u].rs);
//backstack;
que.push((Rec){dfn[u]-arr[u]});
// printf("nod=%d deep=%d dfn=%d\n",u,que.top().deep,dfn[u]);
while(que.top().deep==dfn[u]-1&&!que.empty()){
if(u==node[node[u].fa].ls){
// printf("l:fa=%d\n",node[u].fa);
ans_1[node[u].fa]++;
}else{
// printf("r:fa=%d\n",node[u].fa);
ans_2[node[u].fa]++;
}
que.pop();
}
}
int main(){
freopen("node.in","r",stdin);
freopen("node.out","w",stdout);
scanf("%d",&n);
memset(node,0,sizeof(node));
for(int i=1;i<=n;++i){
scanf("%d",&arr[i]);
}
for(int i=1;i<=n;++i){
scanf("%d",&node[i].ls);
scanf("%d",&node[i].rs);
node[node[i].ls].fa=i;
node[node[i].rs].fa=i;//record father
}
pre(1,0);
dfs(1);
// for(int i=1;i<=n;++i)printf("%d ",dfn[i]);
for(int i=1;i<=n;++i){
printf("%d %d\n",ans_1[i],ans_2[i]);
}
}
Task 2:小奇的日程表
【問題描述】
放暑假了,小奇準備了⼀個日程表來安排他的暑假⽣活.
⼀共有n 件事情,編號為\(1,2......n\),第\(i\) 件事情的難度為\(i\). 小奇將整個暑假劃分為m 個時刻,並設定了三個正整數\(a,b,c.\) 然後,小奇定義了⼀個數列\({ xi }\),滿⾜:
$x[0] = 0 $
\(x [i] = (a x[ i - 1 ] + b) \% 2nc (1 <= i <= m)\)
即從x1 開始,數列的每⼀項等於上⼀項的a 倍加上b 以後除以2nc 的餘數.
在暑假剛開始時,小奇的日程表是空的. 第i 個時刻前,小奇會根據xi 的值決定日程表的變化:
若\(xi < nc\),則將編號為$⌊ xi / c ⌋ + 1 $的事件加⼊日程表,若日程表已有該事件則忽略;
若\(xi >= nc\),則將編號為$⌊ xi / c ⌋ - n + 1 $的事件從日程表刪除,若日程表沒有該事件則忽略;
第i 個時刻\((1 <= i <= m)\),小奇所做的事情就是該時刻日程表中的所有事件.
對於每個時刻,小奇定義該時刻的⼯作量為該時刻做了⼏件事情,該時刻的疲勞度為該時刻做的所有事情的難度之和. 整個暑假小奇的⼯作量為所有時刻的⼯作量之和,疲勞度為所有時刻的疲勞度之和.
請根據$n,m,a,b,c $計算小奇這個暑假的⼯作量和疲勞度.
【輸入格式】
輸⼊⽂件名為\(schedule.in\)
輸⼊⼀⾏,五個由空格分隔的正整數\(n,m,a,b,c\)
【輸出格式】
輸出⽂件名為\(schedule.out\)
⼀⾏兩個由空格分隔的整數,第⼀個數為小奇這個暑假的⼯作量,第⼆個數為小奇這個暑假的疲勞度.
由於答案可能很⼤,請輸出答案除以1000000007 的餘數.
【樣例輸入1】
3 6 4 1 5
【樣例輸出1】
8 13
【樣例輸入2】
431942 2000000 324635 9496472 24439
【樣例輸出2】
879995658 63186390
【樣例1 說明】
由\(x0 = 0,xi = (4x[ i - 1 ] + 1) mod 30(1 <= i <= 6)\)
可推得\(x1 = 1,x2 = 5,x3 = 21,x4 = 25,x5 = 11,x6 = 15\).
第1 個時刻,日程表加⼊事件1,該時刻的事件有{ 1 },⼯作量為1,疲勞度為1.
第2 個時刻,日程表加⼊事件2,該時刻的事件有{ 1,2 },⼯作量為2,疲勞度為1 + 2 = 3.
第3 個時刻,日程表刪除事件2,該時刻的事件有{ 1 },⼯作量為1,疲勞度為1.
第4 個時刻,日程表刪除事件3,該時刻的事件有{ 1 },⼯作量為1,疲勞度為1.
第5 個時刻,日程表加⼊事件3,該時刻的事件有{ 1,3 },⼯作量為2,疲勞度為1 + 3 = 4.
第6 個時刻,日程表刪除事件1,該時刻的事件有{ 3 },⼯作量為1,疲勞度為3.
因此總⼯作量為1+2+1+1+2+1 = 8,總疲勞度為1+3+1+1+4+3 = 13.
【資料規模與約定】
- \(對於40\% 的資料,n,m <= 10^3.\)
- \(對於60\% 的資料,n,m <= 10^5.\)
- \(另外有20\% 的資料,保證所有刪除均可忽略.\)
- \(對於100\% 的資料,n <= 5*10^7,m <= 2*10^6,a <= 10^6,b <= 10^9,c <= 5*10^4.\)
語文題,不用動腦子,動腦子會繞彎,題目讓幹啥你就幹啥就可以了。
有些人想的是用set來儲存,但仔細觀察會發現實際上只需要一個bool陣列就可以了,還不會爆空間,多好啊。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define MAXM 2000010
#define MAXN 50000010
#define lint long long
using namespace std;
lint n,m,a,b,c,ans_1,ans_2,sum_1,sum_2,arr[MAXM],f[MAXM];
bool vis[MAXN];
int main(){
freopen("schedule.in","r",stdin);
freopen("schedule.out","w",stdout);
cin>>n>>m>>a>>b>>c;
const lint modd=n*c*2,cmp=n*c;
for(register int i=1;i<=m;++i){
f[i]=(a*f[i-1]+b)%modd;
if(f[i]<cmp){
int todo=f[i]/c+1;
if(!vis[todo]){
vis[todo]=true;
sum_1+=1;
sum_2+=todo;
}
}else{
int todo=f[i]/c-n+1;
if(vis[todo]){
vis[todo]=false;
sum_1-=1;
sum_2-=todo;
}
}
ans_1=(ans_1+sum_1)%1000000007;
ans_2=(ans_2+sum_2)%1000000007;
}
printf("%lld %lld",ans_1,ans_2);
}
Task 3:送分題
【問題描述】
給定⼀棵N 個節點的樹,每個節點上有⼀個權值。
你要從中選出⼀些點使得權值和最⼤,任意2 個選出的節點之間的距離都要⼤於K。
【輸入格式】
輸⼊⽂件名為score.in
第⼀⾏兩個整數\(N,K\)。
接下來⼀⾏\(N\) 個整數,表示第\(i\) 個節點的權值
接下來\(N - 1\) ⾏,每⾏2 個數\(a,b\),表示點\(a\)和點\(b\) 之間有邊相連
【輸出格式】
輸出⽂件名為\(score.out\)
輸出⼀⾏,表示最⼤的權值和。
【樣例輸入1】
3 1
1 1 1
1 2
1 3
【樣例輸出1】
2
【樣例輸入2】
3 2
1 1 1
1 2
1 3
【樣例輸出2】
1
【資料範圍】
\(n∈[1,10000],k∈[1,100]\)
這是一道送命題。
狀態可以很容易就想到:設\(f[ u ][ j ]\)為當前位於點\(u\),子樹裡最近的選中節點離自身距離大於等於\(j\)。
但是狀態的轉移設計起來就相當麻煩,或者說,說起來容易寫起來難。思路如下:
- 對於\(j*2>k\)的時候,我們可以隨便選著轉移。(直接累加求\(max\)即可)
對於\(j*2<=k\)的時候,我們需要這樣考慮:
- 選出一個點距離為\(j\)
- 其它點的距離都應該大於\(k-j\)
- 顯然除了有一個點距離為j,其它點距離都是k-j
- 考慮預先累加求一定距離時的權值和,需要開一個輔助陣列tmp
- 轉移結束後需要維護一下字尾最大值,讓f[ u ][ j ]是子樹點離u點距離>=j的最大權值
- 其餘的就是常規的樹上DP了
不寫一遍你不會知道這個看起來簡單的思路寫起來有多難受,至少狀態轉移方程的推導真的讓人無從下手。思路我最開始都能想出來,但是實現不了。結果看題解的程式碼,完全就是有機會要上,沒機會創造機會也要硬上。。我果然還是Naive啊。。
Code:程式碼裡註釋很清晰
#include<cstdio>
#include<cstring>
#define MAXN 10010
int n,dis,cnt,tmp[110],v[MAXN],g[MAXN][110],head[MAXN];
struct edge{
int nxt;
int to;
}e[MAXN<<1];
inline void add(int from,int to){
e[++cnt].nxt=head[from];
e[cnt].to=to;
head[from]=cnt;
}
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x<y?x:y;}
void dfs(int u,int fa){
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v!=fa){
dfs(v,u);
}
}//先到葉節點,回溯處理
memset(tmp,0,sizeof(tmp));//提前清空輔助陣列
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
g[u][0]+=g[v][dis-1];
}
g[u][0]+=v[u];
//選中本身的操作和其它點的距離dis對應
tmp[0]=g[u][0];
for(int k=0;k<=dis;++k){
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
tmp[k+1]+=g[v][k];
}//tmp輔助儲存到子樹距離為[0,dis]情況的權值和
}
for(int j=0;j<=dis;++j){
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(dis>=(j<<1)){
g[u][j]=max(g[u][j],tmp[dis-j]-g[v][dis-j-1]+g[v][j-1]);
}//若j不足以直接多次選擇,本次點又要考慮選距離為j的情況
//就通過這種方法選擇
}
if((j<<1)>=dis){
g[u][j]=max(g[u][j],tmp[j]);
}//反之,如果可以直接處理,事情就變得相當簡單。
}
for(int j=dis;j>=0;j--){
g[u][j]=max(g[u][j],g[u][j+1]);
}//維護一個字尾最大值
}
int main(){
freopen("score.in","r",stdin);
freopen("score.out","w",stdout);
scanf("%d%d",&n,&dis);dis++;//輸入時把dis+1便於計算
for(int i=1;i<=n;++i){
scanf("%d",&v[i]);//輸入每個點的權值
}
for(int i=1;i<n;++i){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);//建邊
add(v,u);
}
dfs(1,0);//基於dfs的樹上DP
int ans=0;
for(int i=0;i<=dis;++i){
ans=max(ans,g[1][i]);//獲取答案
}
printf("%d\n",ans);
}