1. 程式人生 > 實用技巧 >SSF資訊社團12月訓練題目

SSF資訊社團12月訓練題目

Week 1(12.13 ~ 12.19)

1427C

Link

考慮一個 \(\mathcal O(n^2)\) 的樸素 dp:令 \(f(i)\) 表示從起點到點 \(i\) 且剛好在點 \(i\) 看到明星的最優答案,則 \(f(i)=\max\limits_{1\le j<i\text{且}\operatorname{dis}(i,j)\le t_i-t_j} \{f_j+1\}\),其中 \(\operatorname{dis}(i,j)=\vert x_i-x_j\vert+\vert y_i-y_j\vert\)

觀察題目以及不等式 \(\operatorname{dis}(i,j)\le t_i-t_j\)

,可以看出當 \(i-j>2r\) 時,\(j\) 一定能夠到達 \(i\)\(\operatorname{dis}(i,j)\) 最多是 \(2r\)\(\{t_i\}\) 單調遞增)。也就是說,\(\max\limits_{1\le j< i-2r}\{f_j\}\) 一定能夠轉移到 \(f_i\),dp 過程中順便存一下即可,而我們需要列舉的只有 \([i-2r+1,i-1]\) 這個區間,這樣複雜度就變成了 \(\mathcal O(nr)\)

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=2e5;
int dp[N+10],t[N+10],x[N+10],y[N+10];
int main()
{
	int n,r;
	scanf("%d %d",&r,&n);
	x[0]=y[0]=1;
	for(int i=1;i<=n;i++) scanf("%d %d %d",&t[i],&x[i],&y[i]);
	int mx=0xcfcfcfcf; //極小值
	memset(dp,0xcf,sizeof(dp));
	dp[0]=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=max(0,i-2*r+1);j<i;j++)
		{
			if(t[i]-t[j]>=abs(x[i]-x[j])+abs(y[i]-y[j]))
				dp[i]=max(dp[i],dp[j]+1);
		}
		dp[i]=max(dp[i],mx+1);
		if(i>=r*2) mx=max(mx,dp[i-r*2]);
	}
	int ans=0;
	for(int j=1;j<=n;j++) ans=max(ans,dp[j]);
	printf("%d",ans); 
	return 0;
}

1437C

Link

\(\{t_i\}\) 排序,令 \(f(i,j)\) 表示前 \(i\) 個菜品在前 \(j\) 個時刻的最小代價,則容易得出 \(f(i,j)=\min\{f(i-1,j-1)+\vert j-t_i\vert,f(i,j-1)\}\),分別決策第 \(j\) 秒選或者不選菜品 \(i\)。注意 \(j\) 的迴圈範圍是 \(1\sim 2n\) 而不是 \(1\sim n\),因為在 \(n\) 秒之後還有可能放菜品。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
using namespace std;
const int N=200;
int dp[N+10][2*N+10],a[N+10];
//   選了 i 個菜品,前j秒 
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		int n;
		scanf("%d",&n);
		for(int i=1;i<=n;i++) scanf("%d",&a[i]);
		sort(a+1,a+n+1);
		memset(dp,0x3f,sizeof(dp));
		for(int i=0;i<=2*n;i++) dp[0][i]=0;
		
		for(int i=1;i<=n;i++)
			for(int j=1;j<=2*n;j++)
				dp[i][j]=min(dp[i][j-1],dp[i-1][j-1]+abs(j-a[i]));
		printf("%d\n",dp[n][n*2]);
	}
	return 0;
}

1437D

Link

一個顯然的性質:若 \(i>k\)\(a_k>a_i\),則 \(a_k\) 不可能是 \(a_i\) 的兄弟節點(也就是說沒有共同的父親),因為題目保證遍歷過程中子節點按照升序遍歷。我們可以用這個性質進行貪心。

具體地,維護一個佇列,將根節點插入佇列,然後用類似 BFS 的方法遍歷處理每一個節點。對於每一個隊頭 $ a_i$,我們在 $ a_i$ 後面找一個最長的子序列 \(a_{j},a_{j+1},\cdots,a_{k}(i< j\le k\le n)\),滿足 \(a_j\) 是第一個未被挑選的點, \(a_{j},a_{j+1},\cdots,a_{k}\) 未被其他點挑選過且這個序列單調上升,這 \(k-j+1\) 個數就是 \(a_i\) 的子節點。類似 BFS,將這些點插入佇列,然後開始下一輪求解。

容易得出這樣選一定是最優的。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=2e5;
int que[N+10],h=1,t=0,d[N+10],a[N+10];
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{ 
		h=1;t=0;
		memset(d,0,sizeof(d));
		memset(a,0,sizeof(a));
		memset(que,0,sizeof(que));
		int n,pos=0;
		scanf("%d",&n);
		for(int i=1;i<=n;i++) scanf("%d",&a[i]);
		que[++t]=1;
		d[1]=0;pos=2;
		int ans=0;
		while(h<=t)
		{
			int x=que[h++];
			ans=max(ans,d[x]);
			int j;
			for(j=pos;j<=n;j++)
				if(j!=pos && a[j]<a[j-1])
					break;
			for(int i=pos;i<j;i++)
			{
				
				d[a[i]]=d[x]+1;
				que[++t]=a[i];
			}
			pos=j;
		}
		printf("%d\n",ans);
	} 
	return 0;
}

1444A

Link

考慮什麼情況下 \(x\) 能夠滿足 \(x\) 不是 \(q\) 的倍數。顯然,若將他們分解成 \(\prod_{i=1}^m p_i^{c_i}\) 的質因數分解形式,則 \(x\) 的某一個 \(c_i\) 一定小於 \(q\)\(c_i\),為了使 \(x\) 最大,它的 \(c_i\) 等於 \(q\)\(c_i-1\),其他質因數及其指數和 \(p\) 一樣。列舉這個 \(c_i\),統計哪個 \(c_i\) 能使 \(x\) 最大即可。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstring>
using namespace std;
#define int long long
int p;int q;
int qpow(int a,int n)
{
	int ans=1;
	while(n)
	{
		if(n&1) ans*=a;
		a*=a;
		n>>=1;
	}
	return ans;
}
int init()
{
	int ans=0x3f3f3f3f3f3f3f3f;
	for(int i=2;i*i<=q;i++)
	{
		if(q%i) continue;
		int c1=0,c2=0;
		while(q%i==0)
		{
			c1++;
			q/=i;
		}
		while(p%i==0)
		{
			c2++;
			p/=i;
		} 
		ans=min(ans,qpow(i,c2-c1+1)); 
	}
	if(q>1)
	{
		int i=q;
		int c1=0,c2=0;
		while(q%i==0)
		{
			c1++;
			q/=i;
		}
		while(p%i==0)
		{
			c2++;
			p/=i;
		} 
		ans=min(ans,qpow(i,c2-c1+1)); 
	}
	return ans;
} 
void read(int &x)
{
	x=0;int f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
	x*=f;
}
void write(int x)
{
	if(x<0)
	{
		putchar('-');
		x=-x;
	}
	if(x>9)write(x/10);
	putchar(x%10^48);
}
signed main()
{
	int T;
	read(T);
	while(T--)
	{
		read(p);read(q);
		if(p%q)
		{
			write(p);
			putchar('\n');
		}
		else
		{
			write(p/init());
			putchar('\n');
		}
	}
	return 0;
}

1446B

Link

\(f(i,j)\) 表示 \(A\) 的前 \(i\) 位,\(B\) 的前 \(j\) 位能夠得到的最大分數,則容易得到轉移方程:

\[f(i,j)=\begin{cases}\max\{f(i,j-1),f(i-1,j)\}-1, &A_i\not=B_j\\ f(i-1,j-1)+2, &A_i=B_j\end{cases} \]

注意過程中分數有可能是負數,隨時和 \(0\)\(\max\) 即可。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=5000;
int dp[N+10][N+10];
char s1[N+10],s2[N+10];
int main()
{
	int n,m,ans=0;
	scanf("%d%d%s%s",&n,&m,s1+1,s2+1);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
			dp[i][j]=max(0,max(dp[i][j-1],dp[i-1][j])-1);
			if(s1[i]==s2[j]) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+2);
			ans=max(ans,dp[i][j]);
		}
	printf("%d",ans);
}

1455D

Link

Lemma:若 \(a_i>x\) 但二者不交換,則後面的數無法通過交換使序列有序。

Prove:取 \(i<j\le n\),假設 \(a_i,a_j\) 都能與 \(x\) 交換,即 \(a_i>x,a_j>x\)
\(a_i\) 不交換但 \(a_j\) 交換,則交換後的 \(a_j'=x<a_i\),這與題目要求的單調不降不符,而且這兩個數無法再次還原,所以 \(a_i\)\(x\) 必須交換。
\(\mathcal{Q.E.D.}\)

Lemma,我們可以掃一遍序列,對於每個 \(a_i\),一旦可以與 \(x\) 交換就執行操作同時累加次數,若執行過程中序列已經有序,輸出答案並直接退出;若序列已經掃描完畢但仍然不是有序的,說明無解,輸出 -1

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=500;
int a[N+10],n;
bool check()
{
	for(int i=2;i<=n;i++)
		if(a[i]<a[i-1])
			return false;
	return true;
}
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		int x;
		scanf("%d %d",&n,&x);
		for(int i=1;i<=n;i++) scanf("%d",&a[i]);
		if(check())
		{
			printf("0\n");
			continue;
		}
		int ans=0;
		for(int i=1;i<=n;i++)
		{
			if(a[i]>x)
			{
				swap(a[i],x);
				ans++;
			}
			if(check()) 
			{
				printf("%d\n",ans);
				break;
			}
		}
		if(!check()) printf("-1\n");
	}
	return 0;
}

Week 2(12.20 ~ 12.27)

1462F

Link

列舉哪個線段與其他所有線段重合,並通過樹狀陣列計算與之重合的線段的數量,數量最多的即為所求線段。

至於如何統計,可以將所有線段以左端點為第一關鍵字、右端點為第二關鍵字排序,對於每一個線段 \([l_i,r_i]\),左端點小於它的(\([l_1,r_1],[l_2,r_2],\cdots,[l_{i-1},r_{i-1}]\)),統計右端點處於區間 \([l_i,+\infty)\) 的有多少個;左端點大於它的(\([l_{i+1},r_{i+1}],[l_{i+2},r_{i+2}],\cdots,[l_n,r_n]\)),統計左端點處於區間 \([l_i,r_i]\),二者相加的和即與該線段重合的線段的數量。由於 \(1 \leq l \leq r \leq 10^9\),樹狀陣列統計前需要離散化。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
using namespace std;
const int N=2e5;
struct segment
{
	int l,r;
	segment() {}
	bool operator < (const segment &x) const {return l==x.l?r<x.r:l<x.l;}
}a[N+10];
int c1[N*2+10],c2[N*2+10],n,m,t[N*2+10];
void modify(int *c,int x,int d) {for(;x<=m;x+=x&-x) c[x]+=d;}
int query(int *c,int x)
{
	int ans=0;
	for(;x;x-=x&-x) ans+=c[x];
	return ans;
}
int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		int n;
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d %d",&a[i].l,&a[i].r);
			t[i*2-1]=a[i].l;
			t[i*2]=a[i].r;
		}
		sort(a+1,a+n+1);
		sort(t+1,t+n*2+1);
		m=unique(t+1,t+n*2+1)-t-1;
		for(int i=0;i<=m;i++)
		{
			c1[i]=0;
			c2[i]=0;
		}
		for(int i=1;i<=n;i++)
		{
			a[i].l=lower_bound(t+1,t+m+1,a[i].l)-t;
			a[i].r=lower_bound(t+1,t+m+1,a[i].r)-t;
		}
		int ans=0;
		for(int i=1;i<=n;i++) modify(c2,a[i].l,1);
		for(int i=1;i<=n;i++)
		{
			ans=max(ans,query(c1,m)-query(c1,a[i].l-1)+query(c2,a[i].r)-query(c2,a[i].l-1));
			modify(c2,a[i].l,-1);
			modify(c1,a[i].r,1);
		}
		printf("%d\n",n-ans);
	}
	return 0;
}

1442B

拿到題沒有思路,然後大力猜結論,本來以為 WA On Test 2,結果改一個取模就 AC 了,RP 真好((

觀察到如果想把某個數 \(a_i\) 加入 \(b\),則一定要在 \(a_{i-1}\)\(a_{i+1}\) 中刪掉一個。在紙上畫畫後我們發現一個規律:

\(p_i\) 表示 \(b_i\)\(\{a_i\}\) 中的下標,\(t_i\) 表示 $ a_i$ 在 \(\{b_i\}\) 中的下標(如果 \(a_i\notin \{b_i\}\) 則令 \(t_i=0\))。
列舉 \(b_i\),令當前 \(b_i\) 對答案的貢獻 \(c_i=0\),若 \(p_i>1\)\(t_{p_i-1}<t_{p_i}\)\(c_i\) 加一;類似地,若 \(p_i<n\)\(t_{p_i+1}<t_{p_i}\)\(c_i\) 加一。
\(\prod_{i=1}^m c_i\) 即為所求。

大白話版本:每個 \(a_i\) 都找到自己在 \(\{b_i\}\) 中是第幾個被“提到”的(就是 \(t_i\)),如果左邊的數沒被提及過或者說左邊的數比他提及的早,\(c_i\gets c_i+1\)(即 c[i]++);右邊做同樣的操作,最後把 \(c_i\) 乘起來就是答案。如果還無法理解可以直接翻下面的程式碼。

接下來證明一下為什麼這樣做是對的。

仔細思考一下,對於這個做法我們的迷惑點實際上是這樣的:如果旁邊的數已經前面的數刪了,並且比當前這個數晚提及,那麼答案就是錯的。但實際上,這個命題是不存在的。以右邊為例,如果發生這種情況,刪除過程中一定會有一個情形:

若 $ a_i$ 為當前正在計算的數,則此時 \(t_{i}>t_{i+1}<t_{i+2}\)

對於這種情況,根據前面的演算法,\(c_{i+1}=0\)。因為計算總貢獻時是 \(\prod\) 而不是 \(\sum\),所以如果出現這種情況答案肯定是 \(0\),就算 \(c_i\) 計算錯了也無傷大雅;如果最後的答案不是 \(0\),就肯定不會出現這種情況。

\(\mathcal{Q.E.D.}\)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
using namespace std;
const int N=2e5;
int a[N+10],b[N+10],tg[N+10],pos[N+10];
int main()
{ 
	int T;
	scanf("%d",&T);
	while(T--)
	{
		memset(tg,-1,sizeof(tg));
		int n,m;
		scanf("%d %d",&n,&m);
		for(int i=1;i<=n;i++) 
		{
			scanf("%d",&a[i]);
			pos[a[i]]=i;
		} 
		for(int i=1;i<=m;i++) 
		{
			scanf("%d",&b[i]);
			tg[pos[b[i]]]=i;
		}
		int ans=1;
		for(int i=1;i<=m;i++) 
		{  
			int cnt=0;
			if(pos[b[i]]>1 && tg[pos[b[i]]-1]<tg[pos[b[i]]]) cnt++;
			if(pos[b[i]]<n && tg[pos[b[i]]+1]<tg[pos[b[i]]]) cnt++;
			ans*=cnt;
			ans%=998244353;
		}
		printf("%d\n",ans);
	}
	return 0;
}