1. 程式人生 > 其它 >2022XCPC訓練刷題筆記

2022XCPC訓練刷題筆記

目錄

雖然是按日期做的題解,但是不一定那天只做了這點題(堅定的眼神)

以前做的題

CF1562A 數學\取模

給範圍L到R,求裡面兩個數取模最大值

首先肯定是b = R時有最大解,令x = b/2。當a小於x時,b裡可以裝下兩個及以上的a,模數也就很小了。要使模數最大,直接模x+1即可

int main() {
	cin >> t;
	while (t--) {
		cin >> l >> r;
		cout << r % max(r / 2 + 1, l) << '\n';
	}
	return 0;
}

CF edu123 C 最大子段和+整體化思想

每次修改是加一個x進去,加到原序列某個位置上,不能重複加。修改n次,每次求一個最大子段和

我把n方斃了,實際上資料是恰好,正解n方。

考慮特殊性質,x不變且為正數,應該貪心地加入它。第i次修改,那麼這次的最大子段和的序列裡面一定包含了i*x。

求不同長度的最大子段和,遍歷i的時候,遍歷不同長度j的最大子段和,往裡面加入i*x再求最優,最優答案可能在遍歷j的外面,因此注意最優答案是在整體狀態空間上的。

	while(t--)
	{
		scanf("%d%d",&n,&x);
		for(int i=1;i<=n;i++) 
		{
			scanf("%d",a+i);
			sum[i] = sum[i-1] + a[i];
			f[i] = -0x7f7f7f7f;
		}
		for(int i=1;i<=n;i++)
			for(int j=i;j<=n;j++)
				f[j-i+1] = max(f[j-i+1],sum[j]-sum[i-1]);//不同長度最大子段和
		int ans = 0;
		for(int i=0;i<=n;i++)
		{
			for(int j=i;j<=n;j++) 
				ans = max(ans, f[j]+i*x);
			printf("%d ",ans);
		}
		puts("");
	}

子串的最大差 單調棧+計算貢獻

我現在覺得單調棧就不要糾結單調增還是單調減了

要求得每個元素在多少個子串中是最大值

對於單個元素:(左邊比它小的個數,小於等於)* (右邊比它小的個數,僅小於)

直接用棧來儲存位置,對於ai,一直彈出棧頂元素直到棧頂比它大,這樣就找到了最靠左的位置使得ai在這個區間裡最大,又從右往左做一遍

最小值同理

int n,a[N];
int posl[N],posr[N];//記錄左右兩邊更大值的位置
int stk[N],top;
ll ans = 0;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",a+i);
	a[0] = a[n+1] = inf;
	for(int i=1;i<=n;i++)
	{
		while(a[stk[top]] <= a[i])//單調增找最大
			top--;
		posl[i] = stk[top];
		stk[++top] = i;//存位置
	}
	top = 0;
	stk[0] = n+1;
	for(int i=n;i>=1;i--)
	{
		while(a[stk[top]] < a[i])//避免重複
			top--;
		posr[i] = stk[top];
		stk[++top] = i;
	}
	for(int i=1;i<=n;i++) ans += 1ll * (posr[i]-i) * (i-posl[i]) * a[i];
	//負的貢獻再做一遍
	a[0] = a[n+1] = -inf;
	stk[0] = 0;
	for(int i=1;i<=n;i++)
	{
		while(a[stk[top]] >= a[i])//單調減找最小
			top--;
		posl[i] = stk[top];
		stk[++top] = i;//存位置
	}
	top = 0;
	stk[0] = n+1;
	for(int i=n;i>=1;i--)
	{
		while(a[stk[top]] > a[i])
			top--;
		posr[i] = stk[top];
		stk[++top] = i;
	}
	for(int i=1;i<=n;i++) ans -= 1ll * (posr[i]-i) * (i-posl[i]) * a[i];
	printf("%lld",ans);
}

CF793D 區間dp

從外往裡做的一個區間dp,設ij是可以去的區間

https://www.luogu.com.cn/blog/xzggzh/solution-cf793d

細節太多了,好難受

(鴿巢原理)從n個數中選出幾個數和為n的倍數

選數

題意:

給定一個長度為$$n(n\le 10^5) $$的陣列a,你需要選擇其中一些數之和為n的倍數

思路:

首先n的倍數模n為0,考慮到選數,利用求模線性的特點,我們把不同可能性的和限制在0到n之間

這就引出了抽屜原理(有點靠直覺),同時要想到字首和,由於只關心0到n之間的值,字首和通通模上n

  • 若字首和模以n為0,那麼從第一個數到他之和就是答案

  • 字首和兩個相同數位置之間的這一段和就是答案,因為這意味著這一段的和模以n得0

  • 字首和陣列算上第0位有n+1個數字,由於抽屜原理一定有兩個相同的數,一定有有解

為了找這個相同的,用個桶就行

void solve()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		pre[i] = (pre[i-1] + a[i]) % n;
	}
	int ansl = 0,ansr = 0;
	for(int i=1;i<=n;i++)
	{
		if(pre[i] == 0)
		{
			ansl = 1,ansr = i;
			break;
		}
		if(pos[pre[i]])
		{
			ansl = pos[pre[i]] + 1, ansr = i;//別忘了字首和對應的區間
			break;
		}
		else pos[pre[i]] = i;
	}
	printf("%d\n",ansr-ansl+1);
	for(int i=ansl;i<=ansr;i++)
	{
		printf("%d ",i);
	}
}

離線序列操作+利用性質

序列操作

題意:

給序列,操作1:把第$$x$$個數改為\(y\);操作2:把所有小於\(y\)的數覆蓋為\(y\)

思路:

誤區:一下進入資料結構加維護的思路里,忽略了各種操作的後效性

考慮離線化操作,看看操作的性質,對於覆蓋的操作2,只有改成最大的值答案才能留在最後,對於操作1,最後一次對\(x\)的操作才會留在最後。注意到無論操作1還是2,都會對x位置進行一個嘗試修改

那麼如何處理兩種操作讓他們的相互影響在離線化的情況下被統計呢?
由於有“最值”、“最後”這種字眼,我們倒著列舉操作,儲存最大的覆蓋值,往前看操作,如果是更小的覆蓋就沒用,如果對當前值的修改更小也沒用

對於從來沒有過修改的值輸出$$a[i]$$即可

我們用一個$$ans$$陣列儲存修改後的答案,若為空則看是$$maxcover$$大還是$$a[i]$$大

void solve()
{
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=q;i++)
	{
		scanf("%d",&op[i][0]);
		if(op[i][0]==1) scanf("%d%d",&op[i][1],&op[i][2]);
		else scanf("%d",&op[i][1]);
	}
	int maxcover = -1;
	for(int i=q;i>=1;i--)
	{
		if(op[i][0] == 2) maxcover = max(maxcover,op[i][1]);
		else
		{
			if(ans[op[i][1]]) continue;
			ans[op[i][1]] = max(maxcover,op[i][2]);
		}
	}
	for(int i=1;i<=n;i++)
	{
		if(a[i] > maxcover && a[i] > ans[i] && ans[i]==0) printf("%d ",a[i]);//考慮未被修改的值!
		else if(!ans[i]) printf("%d ",maxcover);
		else printf("%d ",ans[i]);
	}
}

查詢區間中比x小的元素個數

題意:

給出一個序列,每次查詢\([L,R]\)區間中\(\le H\)的元素個數。

思路:

原本是主席樹板子題,但是有神奇辦法線段樹可以切掉

考慮線段樹同時維護區間的最大最小值,如果一個區間的最大值比\(H\)小,那麼整個區間的數計入答案,如果一個區間最小值比\(H\)大,那麼這個區間無法計入答案,如果\(H\)介於最大最小之間,那麼就按照線段樹的分治思想分開遞迴統計

核心在於線段樹的\(query()\)函式怎麼寫

struct segment_tree{
	int maxx[N<<2],minn[N<<2];
	#define ls rt<<1
	#define rs rt<<1|1
	inline void pushup(int rt)
	{
		maxx[rt] = max(maxx[ls],maxx[rs]);
		minn[rt] = min(minn[ls],minn[rs]);
	}
	inline void build(int rt,int l,int r)
	{
		if(l==r)
		{
			maxx[rt] = minn[rt] = a[l];
			return;
		}
		int mid = l+r>>1;
		build(ls,l,mid);
		build(rs,mid+1,r);
		pushup(rt);
	}
	inline int query(int rt,int l,int r,int L,int R,int val)
	{
		if(minn[rt] > val) return 0;
		if(L<=l && r<=R)
		{
			if(maxx[rt] <= val) return r-l+1;
		}
		int ret = 0,mid = l+r>>1;
		if(L<=mid) ret += query(ls,l,mid,L,R,val);
		if(R> mid) ret += query(rs,mid+1,r,L,R,val);
		return ret;
	}
}T;
int n,q;
int x,y,z;
void solve()
{
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++) scanf("%d",a+i);
	T.build(1,1,n);
	while(q--)
	{
		scanf("%d%d%d",&x,&y,&z);
		printf("%d ",T.query(1,1,n,x,y,z));
	}
	puts("");
}

(貪心)按位或的最小生成樹

Minimum Or Spanning Tree

題意:

最小生成樹,不過是要求邊權按位或起來最小

思路:

按位考慮,貪心地,從最高位開始儘量選擇邊權為0的邊

回顧一下,克魯斯卡爾演算法是儘量選擇邊權最小的邊加入邊集,但這裡的思考我被這個演算法給桎梏了。他應該是個邊集裡刪邊的演算法,我們儘量把高位上為0的邊留下來,為1的刪除出去,直到形成一棵生成樹

具體過程:從高位列舉,再列舉當前邊集,如果該條邊這一位為0就塞進並查集,最後看聯通塊是否為1,如果為1說明還可以再繼續刪且答案的這一位就填0,同時新的邊集就是當前邊集裡這一位為0的邊(也就是刪邊),而如果連通塊不為1說明需要一條這一位為1的邊,那麼就不做刪除,由下一位的列舉決定

struct UF{
	int fa[N],cnt;
	void init()
	{
		for(int i=1;i<=n;i++) fa[i] = i;
		cnt = n;//連通塊數
	}
	int find(int x){return fa[x] = (fa[x] == x) ? x:find(fa[x]);	}	
	void Union(int x,int y)
	{
		int fx = find(x);
		int fy = find(y);
		cnt -= (fx!=fy);
		fa[fx] = fy;		
	}
}uf;
struct edge{int u,v,w;};
void solve()
{
	scanf("%d%d",&n,&m);
	vector<edge> e;
	
	for(int i=1,u,v,w;i<=m;i++)
	{
		scanf("%d%d%d",&u,&v,&w);
		e.push_back({u,v,w});
	}
	int ans = 0;
	for(int i=30;i>=0;i--)
	{
		uf.init();
		vector <edge> ne;//一個邊集逐漸縮小的過程
		for(auto ed: e)
		{
			int u = ed.u, v = ed.v, w = ed.w;
			if(((w>>i)&1) == 0)
			{
				ne.push_back(ed);
				uf.Union(u,v);
			}
		}
		if(uf.cnt == 1)
		{
			ans += (0<<i);
			e = ne;//新的邊集
		}
		else ans += (1<<i);
	}
	printf("%d",ans);
}

3.19

(狀壓)求漢密爾頓迴路的方案數

題意:假設兩個數互質就是兩點有雙向邊,求從1開始遍歷每個點僅一次的方案數

思路:

狀態壓縮令\(S\)的二進位制位01表示到達那個點,如果i位為1就是到過

狀壓的核心是列舉每種狀態,列舉轉移關係,然後構造前序狀態並轉移

\(dp[S][i]\)表示\(S\)狀態下當前走到了\(i\)號點,有\(dp[S][i] += dp[S\_from][j]\),其中\(S\_from\)\(j\)位為\(1\)\(i\)位為\(0\)的構造出的狀態

	dp[1][1] = 1;
	for(int s=0;s<(1<<21);s++)
	{
		for(int i=1;i<=21;i++)
		{
			if((s&(1<<(i-1))) == 0) continue;//s符合i
			for(int j=1;j<=21;j++)
			{
				if((s&(1<<(j-1))) == 0) continue; //s符合j 
				int s_from = s&(~(1<<(i-1)));//刪去i位上的1
				if(G[i][j]) dp[s][i] += dp[s_from][j];
			}
		}
	}
	long long ans = 0;
	for(int i=1;i<=21;i++) ans += dp[(1<<21)-1][i];

(揹包)按順序選數儘可能多選且其之和任意時刻不為負

題意:如題,答案就是最多能選幾個數https://codeforces.com/contest/1526/problem/C1

可以類似成可達性揹包,揹包的值就是為正就是可達,當然要保證做的過程中儘量更大

思路:

\(dp[i][j]\)考慮到前i個數,已經選了j個的最大和,到達不了的先設定為-1,已經選了0個數的設定為0,轉移方程 為\(dp[i][j] = max(dp[i-1][j-1]+a[i],dp[i-1][j])\)表示從選\(a[i]\)與不選裡面最大的轉移過來,當然轉移前的位置必須是可達的

可以滾動陣列,但是還是先把二維的寫熟

	memset(dp,-1,sizeof dp);
	dp[0][0] = 0;
	for(int i=1;i<=n;i++)
	{
		dp[i][0] = 0;
		for(int j=1;j<=i;j++)
		{
			if(dp[i-1][j-1]+a[i]>=0 && dp[i-1][j-1]>=0) //可達性判斷
				dp[i][j] = max(dp[i-1][j-1]+a[i],dp[i-1][j]);
			else dp[i][j] = dp[i-1][j];
		}
	}
	for(int i=1;i<=n;i++) if(dp[n][i]>=0) ans = i;//答案為最大的可選的個數

3.20

翻轉01串改變1的個數的情況數

arc137B

題意:

給定一個01串,你可以翻轉區間\([L,R]\)的數,計算一次操作後,01串中1的總數有多少種

思路:

當時在場上瘋狂摸例子但是卻沒有再深入一步,是經驗不足。

考慮一次操作對1的個數的改變值,1變為0,使得1的個數-1,0變為1,則使得1的個數+1

這樣我們就把原01串化成一個運算元組:令1用-1代替,0用1代替,對這個陣列做子段和就是這個子段裡原01串翻轉後1增加的個數,那麼做最大子段和就是整個串所能增加的最大的1的個數,由於每個值都是加減1,我們有理由相信最後可以取遍 原1的個數最大1的個數 的所有值

對於減少的1的個數,我們把1和-1對換,再做一遍

int solve()//最大子段和
{
	int maxx = 0,sum = 0;
	for(int i=1;i<=n;i++)
	{
		sum += a[i];
		if(sum < 0) sum = 0;
		maxx = max(maxx,sum);
	}
	return maxx;
}
int ans1,ans2;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",a+i);
	for(int i=1;i<=n;i++)
	{
		if(a[i] == 1) a[i] = -1;
		else a[i] = 1;
	}
	ans1 = solve();
	for(int i=1;i<=n;i++) a[i] = -a[i];
	ans2 = solve();
	printf("%d",ans1+ans2+1);
	return 0;
}

(博弈論)觀察運算元的奇偶性

arc137C

題意:

每次取數列中最大值,把它縮小成另一個數,但要求變化後每個數各不相同。每個數都是非負整數。

思路:

屬實看不明白,當搬運工了

考慮最大值\(max\)和次大值\(smax\),為了方便思考,簡化成只有他倆的數列,若\(max = smax+1\)則先手必輸,若\(max>smax+1\)則先手必定把它改成\(smax+1\)(運算元不為0),此時先手必贏。

如果 \(a[n]>a[n-1]+1\)

如果先手可以讓\(a[n]\) 變成小於 \(a[n-1]\)的數,並且這樣做可以贏的話,那先手這樣做就可以了。

如果讓 \(a[n]\)變成小於\(a[n-1]\) 的數不能獲勝的話,說明這樣必輸,先手可以讓\(a[n]=a[n-1]+1\) ,這樣可以強制讓後手執行\(a[n]\)** 變成小於\(a[n-1]\)的數**,後手必輸則先手必贏。

如果\(a[n]=a[n-1]+1\)

為了贏取操作空間雙方應該會使運算元儘量多,所以我們考慮最大運算元

最後的結果數列一定是\(0,1,2,3......n-1\),每次都操作最大值,所以最大運算元是\(max-(n-1)\)

結合最簡模型,最大運算元為奇數先手必贏,偶數後手必贏。