1. 程式人生 > 其它 >[Contest on 2021.10.9] 說好的 100kb 呢?

[Contest on 2021.10.9] 說好的 100kb 呢?

咕了好久了... 待辦 list 終於少了一項。 目錄

牛表

題目描述

給出三個整數 \(x, y, P(1 ≤ x, y < P)\)\(P\) 為素數。可以重複對 \(x\) 執行如下操作: 選擇一個整數 \(z ∈ [1, P − 1]\),花費 \(∣ x − z ∣\) 的牛幣,使得 \(x = x \cdot z \bmod P\)

最小需要花費多少牛幣才能使得 \(x = y\)? 設 \(ans(i, j)\) 為 當 \(x = i, y = j\) 時的答案,為了減少輸出,你需要輸出 \(\sum_{i=1}^{P-1}\sum_{i=1}^{P-1} ans(i,j)\cdot t^{(i-1)\cdot (P-1)+j-1}\bmod 998244353\)

\(2\le P\le 2000,1\le t\le 5000\)

解法

首先可以 \(\mathcal O(n^3)\)\(\rm Floyd\)。打表可知,\(ans(i,j)\le 17\),這就意味著邊數不會超過 \(35n\)。用堆優化 \(\rm Dijkstra\)

可以做到 \(\mathcal O(35n^2\log n)\)

不過複雜度可以優化到更優。

還是由於 \(ans(i,j)\le 17\),所以邊權範圍很小,令其為 \(D\)。可以開 \(D\) 個佇列,複雜度是 \(\mathcal O(n^2D)\) 的。這完全等於沒講

考慮 \(\rm Dijkstra\) 需要重複入隊和優先佇列維護的原因是邊權不同,可能原來更劣的解加上新的邊權就更優了。開 \(D\) 個佇列,哪種邊就塞進哪個佇列就規避了這個問題。具體更新過程:找到 \(D\) 個佇列中當前離 \(s\)(源點)最近的點,列舉 \(2D\) 種邊權來擴充套件,塞入對應邊權的佇列。

可以證明從佇列中取出的點的最短路一定是遞增的。可以用歸納法,先開始最短路是遞增的,據此可以證明每個佇列的值是遞增的,反過來就可以證明原命題。

所以每個點只被更新一次(可以被入隊多次),只用最優值來更新即可。每個點拓展一次,拓展複雜度 \(\mathcal O(D)\),所以總共是 \(\mathcal O(n^2D)\) 的。

在神仙學弟的程式碼上改了改:\(\rm Link.\)

程式碼

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' or s<'0')
		f |= (s=='-');
	while(s>='0' and s<='9')
		x = (x<<1)+(x<<3)+(s^48),
		s = getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar('-');
		write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
}

#include <queue>
#include <iostream>
using namespace std;

const int inf = 5e6;
const int maxn = 2005;
const int mod = 998244353;

int p,t,head[maxn],cnt;
int dis[maxn];
bool vis[maxn];
struct edge {
	int nxt,to,w;
} e[maxn*20];
struct node {
	int u,w;
	node() {}
	node(int U,int W):u(U),w(W) {}
	
	bool operator < (const node &t) const {
		return w>t.w;
	}
};
priority_queue <node> q;

inline int Abs(int x) {
	return x>0?x:-x;
}

inline int inc(int x,int y) {
	return x+y>=mod?x+y-mod:x+y;
}

void addEdge(int u,int v,int w) {
	e[++cnt].to=v;
	e[cnt].nxt=head[u];
	e[cnt].w=w;
	head[u]=cnt;
}

void Dijkstra(int x) {
	for(int i=1;i<p;++i)
		dis[i]=0x3f3f3f3f,vis[i]=0;
	q.push(node(x,0));
	dis[x]=0;
	while(!q.empty()) {
		node t=q.top(); q.pop();
		if(vis[t.u] or (t.w^dis[t.u]))
			continue;
		vis[t.u]=1;
		for(int i=head[t.u];i;i=e[i].nxt) {
			int v=e[i].to;
			if(dis[v]>dis[t.u]+e[i].w) {
				dis[v]=dis[t.u]+e[i].w;	
				q.push(node(v,dis[v]));
			}
		}
	}
}

int main() {
	p=read(9),t=read(9);
	for(int i=1;i<p;++i) {
		int l=max(1,i-18);
		int r=min(p-1,i+18);
		for(int j=l;j<=r;++j) 
			addEdge(i,1ll*i*j%p,Abs(i-j));
	}
	int tmp=1,ans=0; 
	for(int i=1;i<p;++i) {
		Dijkstra(i);
		for(int j=1;j<p;++j)
			ans = inc(ans,1ll*dis[j]*tmp%mod),
			tmp = 1ll*tmp*t%mod;
	}
	print(ans,'\n');
	return 0;
}

矩陣學說

題目描述

給定 \(n\times m\) 的矩陣 \(a\)。求其中包含的恰好含 \(k\) 個不同整數個數的正方形。

\(1\le n,m\le 1500,1\le a_i\le 100\)

解法

\(\text{Subtask 1}\)\(n\le 500\)

列舉左上角和正方形邊長。用兩個 \(\text{long long}\)\(\rm bitset\) 來維護邊長增加時新增的數字。

\(\text{Subtask 2}\)\(n\le 1500\)

對於固定的左上角,正方形包含數字種類是單調的嗄!二分邊長,可以用二維 \(\rm st\) 表預處理(\(\rm bitset\)),複雜度 \(\mathcal O \left(n^2\log n\cdot \frac{a_i}{w}\right)\)

需要注意的是二維 \(\rm st\) 表只適用於邊長相等的情況。

程式碼

#pragma GCC optimize(2)
#pragma GCC optimize(Ofast)
#include <cstdio>
#define print(x,y) write(x),putchar(y)


template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' or s<'0')
		f |= (s=='-');
	while(s>='0' and s<='9')
		x = (x<<1)+(x<<3)+(s^48),
		s = getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar('-');
		write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
}

#include <bitset>
#include <iostream>
using namespace std;

const int maxn = 1505;

int n,m,K,lg[maxn],lim;
bitset <100> f[12][maxn][maxn];

void init() {
	for(int k=1;k<=lg[lim];++k)
		for(int i=1;i+(1<<k)-1<=n;++i)
			for(int j=1;j+(1<<k)-1<=m;++j)
				f[k][i][j] = (
					f[k-1][i][j]|
					f[k-1][i+(1<<k-1)][j]|
					f[k-1][i][j+(1<<k-1)]|
					f[k-1][i+(1<<k-1)][j+(1<<k-1)]
				);
}

int ask(int u,int d,int l,int r) {
	int dis=lg[r-l+1];
	return (
		f[dis][u][l]|
		f[dis][u][r-(1<<dis)+1]|
		f[dis][d-(1<<dis)+1][l]|
		f[dis][d-(1<<dis)+1][r-(1<<dis)+1]
	).count();
}

int calc(int x) {
	int ret=0;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j) {
			int l=0,r=min(n-i,m-j)+1,mid;
			while(l<r) {
				mid=l+r+1>>1;
				if(ask(i,i+mid-1,j,j+mid-1)<=x)
					l=mid;
				else r=mid-1;
			}
			ret += l;
		}
	return ret;
}

int main() {
	n=read(9),m=read(9),K=read(9);
	lim = max(n,m);
	for(int i=2;i<=lim;++i)
		lg[i]=lg[i>>1]+1;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			f[0][i][j][read(9)-1]=1;
	init();
	print(calc(K)-calc(K-1),'\n');
	return 0;
}

牛牛和陣列操作

題目描述

給定長度為 \(n\) 的數列 \(a\)。你需要做 \(n\) 次操作,每次操作為以下形式:

  • 選擇一個整數 \(x\) 滿足 \(a_x ≠ 0\),將 \(a_x\) 變成 \(0\),令 \(l,r\) 分別為離 \(x\) 向左/右最近的 \(a\)\(0\) 的下標,此次操作的花費為 \(\max\{a_l, a_{l+1}, . . . , a_{x−1}\} + \max\{a_{x+1}, a_{x+2}, . . . , a_{r}\}\) 牛幣。

問有多少不同的操作方式使得操作花費的牛幣最少,兩種操作不同當且僅當兩種操作的操作序列不同。答案對 \(998244353\) 取模。

\(1\le n\le 2000,1\le a_i\le n\)

解法

很難想象正解時間複雜度是 \(\mathcal O(n^3)\)

首先可以發現,一次操作會將一個區間分成互不干擾的兩半,這提醒我們區間 \(\mathtt{dp}\)。不過似乎我們並不需要 \(\mathtt{dp}\) 得出最少花費,可以感性理解每次選擇區間 \(\max\) 作為劃分點是最優的。

合併兩個區間時貢獻就是 \(dp_{i,k-1}\cdot dp_{k+1,j}\cdot \binom{j-i}{k-i}\),就是兩個長度為 \(k-i,j-k\) 的序列保留各自順序進行合併。

另外講一個小 \(\rm trick\)?發現區間 \(\max\) 如果相鄰,它們的總貢獻相當於是其中之一的貢獻乘二。感覺沒啥用

程式碼

#pragma GCC optimize(2)
#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' or s<'0')
		f |= (s=='-');
	while(s>='0' and s<='9')
		x = (x<<1)+(x<<3)+(s^48),
		s = getchar();
	return f?-x:x;
}

template <class T>
inline void write(const T x) {
	if(x<0) {
		putchar('-');
		write(-x);
		return;
	}
	if(x>9) write(x/10);
	putchar(x%10^48);
}

const int maxn = 2005;
const int mod = 998244353;

int pos[maxn][maxn];
int n,a[maxn],dp[maxn][maxn];
int fac[maxn],ifac[maxn];

inline int inc(int x,int y) {
	return x+y>=mod?x+y-mod:x+y;
}

inline int inv(int x,int y=mod-2) {
	int r=1;
	while(y) {
		if(y&1) r=1ll*r*x%mod;
		x=1ll*x*x%mod; y>>=1;
	}
	return r;
}

void init() {
	fac[0]=1;
	for(int i=1;i<=n;++i) {
		pos[i][i]=i;
		dp[i][i]=dp[i][i-1]=dp[i+1][i]=1;
		for(int j=i+1;j<=n;++j)
			if(a[pos[i][j-1]]<a[j])
				pos[i][j]=j;
			else pos[i][j]=pos[i][j-1];
		fac[i]=1ll*fac[i-1]*i%mod;
	}
	ifac[n]=inv(fac[n]);
	for(int i=n-1;i>=0;--i)
		ifac[i]=1ll*ifac[i+1]*(i+1)%mod;
}

inline int C(int n,int m) {
	if(n<m or n<0 or m<0)
		return 0;
	return 1ll*fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}

int main() {
	n=read(9);
	for(int i=1;i<=n;++i)
		a[i]=read(9);
	init(); int F;
	for(int len=2;len<=n;++len)
		for(int i=1;i+len-1<=n;++i) {
			int j=i+len-1,mx=a[pos[i][j]];
			for(int k=pos[i][j];k<=j;k=pos[k+1][j]) {
				if(a[k]^mx) break;
				F = (k==i or k==j)?1:C(len-1,k-i);
				dp[i][j] = inc(dp[i][j],1ll*dp[i][k-1]*dp[k+1][j]%mod*F%mod);
				if(k==j) break;
			}
		}
	print(dp[1][n],'\n');
	return 0;

與巨

題目描述

定義無窮序列 \(f\)\(f_1=1,f_n=f_{n-1}\cdot 2+1\),函式 \(G(x)=\min_{f_i\ge x}f_i\)

定義 \(dp_{c,0}=0\),遞推式:

\[dp_{c,i}=\max\{dp_{c,i-1},[(ic\ \&\ G(i))=i]\cdot i\} \]

\(\sum_{i=0}^n dp_{c,i}\bmod 998244353\)

\(1\le |n|\le 10^7,1\le c\le 10^{18}\)

解法

\(\text{Subtask 1}\):打表

對於 \(|n|\le 30,1\le c\le 50\)

有一個結論:\(dp_{c,i}\)\(c\) 為偶數時恆為 \(0\)。這裡感謝 \(\text{Emm_titan}\) 送出的證明!

  • \(i=0\)。此時值為零。
  • \(i\neq 0\)。找到 \(i\) 最低位的 \(1\) 的位置 \(j\),乘上一個偶數相當於 \(j\) 上的 \(1\) 出現了偶數次!所以第 \(j\) 位就變成 \(0\) 了,而且前面的位都不會更改它了。

因此就只有 \(25\) 個有用的 \(c\)。可以對 \(dp_{c_i,k\cdot 10^6}\) 的字首和進行打表。由於 \(n\)\(10^9\) 範圍,先找到離 \(n\) 最近的 \(k\),再 \(\mathcal O(10^6)\) 地暴算。

\(\text{Subtask 2}\):非打表

我直呼 \(\rm gelivable\)!非常地 \(\rm niubility\)

首先發現 \(G(i)=2^{t+1}-1\)\(t\) 為二進位制最高位。那麼 \(x\text{ and }G(x)=x\bmod 2^{t+1}\)

所以條件可以轉化為 \(i\cdot (c-1)\bmod 2^{t+1}=0\)。將 \(c-1\) 表示為 \(q\cdot 2^p\)\(\gcd(q,2)=1\)),那麼 \(i\) 必須被 \(2^{t+1-p}\) 整除。

由於 \(t\) 至多隻到 \(|n|-1\),我們列舉 \(t\)

  • \(t<|n|-1\)

    此時 \([2^t,2^{t+1})\) 之內的數都在 \(n\) 之內,令 \(g=t+1-p\),那麼 \(i\)\(2^g\) 整除意味著 \(i\) 的低 \(g\) 位全為零。

    那麼滿足條件的數實際上是 \(2^t,2^t+2^g,...,2^t+k\cdot 2^g\)。其中 \(k=2^{t-g}\)。而且每一項都貢獻了 \(2^g\) 次,實際上對於 \(2^t\) 就是 \([2^t,2^t+2^g)\)

  • \(t=|n|-1\)

    和上面類似,只是此時 \(k=\left \lfloor \frac{n}{2^g} \right\rfloor+2^{t-g}\)。最後一項的貢獻次數也多算了,需要減去。

程式碼

咕咕咕…