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

牛表

题目描述

给出三个整数 (x, y, P(1 ≤ x, y < P))(P) 为素数。可以重复对 (x) 执行如下操作: 选择一个整数 (z ∈ [1, P − 1]),花费 (∣ x − z ∣) 的牛币,使得 (x = x cdot z mod 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}mod 998244353)

(2le Ple 2000,1le tle 5000)

解法

首先可以 (mathcal O(n^3))( m Floyd)。打表可知,(ans(i,j)le 17),这就意味着边数不会超过 (35n)。用堆优化 ( m Dijkstra) 可以做到 (mathcal O(35n^2log n))

不过复杂度可以优化到更优。

还是由于 (ans(i,j)le 17),所以边权范围很小,令其为 (D)。可以开 (D) 个队列,复杂度是 (mathcal O(n^2D)) 的。这完全等于没讲

考虑 ( m Dijkstra) 需要重复入队和优先队列维护的原因是边权不同,可能原来更劣的解加上新的边权就更优了。开 (D) 个队列,哪种边就塞进哪个队列就规避了这个问题。具体更新过程:找到 (D) 个队列中当前离 (s)(源点)最近的点,枚举 (2D) 种边权来扩展,塞入对应边权的队列。

可以证明从队列中取出的点的最短路一定是递增的。可以用归纳法,先开始最短路是递增的,据此可以证明每个队列的值是递增的,反过来就可以证明原命题。

所以每个点只被更新一次(可以被入队多次),只用最优值来更新即可。每个点拓展一次,拓展复杂度 (mathcal O(D)),所以总共是 (mathcal O(n^2D)) 的。

在神仙学弟的代码上改了改:( m 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,'
');
	return 0;
}

矩阵学说

题目描述

给定 (n imes m) 的矩阵 (a)。求其中包含的恰好含 (k) 个不同整数个数的正方形。

(1le n,mle 1500,1le a_ile 100)

解法

( ext{Subtask 1})(nle 500)

枚举左上角和正方形边长。用两个 ( ext{long long})( m bitset) 来维护边长增加时添加的数字。

( ext{Subtask 2})(nle 1500)

对于固定的左上角,正方形包含数字种类是单调的嗄!二分边长,可以用二维 ( m st) 表预处理(( m bitset)),复杂度 (mathcal O left(n^2log ncdot frac{a_i}{w} ight))

需要注意的是二维 ( m 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),'
');
	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) 取模。

(1le nle 2000,1le a_ile n)

解法

很难想象正解时间复杂度是 (mathcal O(n^3))

首先可以发现,一次操作会将一个区间分成互不干扰的两半,这提醒我们区间 (mathtt{dp})。不过似乎我们并不需要 (mathtt{dp}) 得出最少花费,可以感性理解每次选择区间 (max) 作为划分点是最优的。

合并两个区间时贡献就是 (dp_{i,k-1}cdot dp_{k+1,j}cdot inom{j-i}{k-i}),就是两个长度为 (k-i,j-k) 的序列保留各自顺序进行合并。

另外讲一个小 ( m 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],'
');
	return 0;

与巨

题目描述

定义无穷序列 (f)(f_1=1,f_n=f_{n-1}cdot 2+1),函数 (G(x)=min_{f_ige 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}mod 998244353)

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

解法

( ext{Subtask 1}):打表

对于 (|n|le 30,1le cle 50)

有一个结论:(dp_{c,i})(c) 为偶数时恒为 (0)。这里感谢 ( ext{Emm_titan}) 送出的证明!

  • (i=0)。此时值为零。
  • (i eq 0)。找到 (i) 最低位的 (1) 的位置 (j),乘上一个偶数相当于 (j) 上的 (1) 出现了偶数次!所以第 (j) 位就变成 (0) 了,而且前面的位都不会更改它了。

因此就只有 (25) 个有用的 (c)。可以对 (dp_{c_i,kcdot 10^6}) 的前缀和进行打表。由于 (n)(10^9) 范围,先找到离 (n) 最近的 (k),再 (mathcal O(10^6)) 地暴算。

( ext{Subtask 2}):非打表

我直呼 ( m gelivable)!非常地 ( m niubility)

首先发现 (G(i)=2^{t+1}-1)(t) 为二进制最高位。那么 (x ext{ and }G(x)=xmod 2^{t+1})

所以条件可以转化为 (icdot (c-1)mod 2^{t+1}=0)。将 (c-1) 表示为 (qcdot 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+kcdot 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} ight floor+2^{t-g})。最后一项的贡献次数也多算了,需要减去。

代码

咕咕咕…

原文地址:https://www.cnblogs.com/AWhiteWall/p/15386700.html