5.5~5.13

区间DP

以一道模板题目为例----石子合并

不能用贪心,会陷入局部最优解。用DP求解

定义dp[i][j]为合并第i堆到第j堆的最小花费

状态转移方程为:

dp[i][j] = min(dp[i][k] + dp[k+1][j] + w[i][j])i <= k < jw[i][j]表示第i堆到第j堆的石子总数,可以用前缀和计算。dp[1][n]就是答案

复杂度O(n3),可以用四边形不等式优化到O(n2)

自顶向下的思路:

计算大区间[i,j]的最优值时,合并它的两个子区间[i][k][k+1][j],对所有可能的合并(i <= k < j)采取最优合并。子区间再分解为更小的区间,最小区间[i,i+1]只包含两堆石子

自底向上的编程:

先在小区间进行DP得到最优解,再逐步合并小区间为大区间。以下是code:

const int INF = 0x3f3f3f3f;
int dp[n][n] {};	//C++11特性,集成初始化为0

for(int len = 2; len <= n; len++){	//len为i到j的区间长度
    for(int i = 1; i <= n - len + 1; i++){	//区间起点i
        int j = i + len - 1;	//区间终点j
        dp[i][j] = INF;		//初始化为INF
        for(int k = i; k < j; k++){
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]);
        }
    }
}

字符串区间

大致题意:两个长度相等的小写字母字符串A和B。定义一次操作:把A的一个连续字串(区间)都转换为某个字符串(就像用刷子刷成一样的字符)。要把A转换成B,所需最少操作数是多少?strlen <= 100

input:

zzzzzfzzzzz

abcdefedcba

output:

6

两部分dp:1.从空白串转换到B。2.从A转换到B

  1. 从空白串转换到B:

B[i] == B[j],原区间[i,j]的最少操作次数 等于 分别去掉两个端点的区间的最少操作次数,例如B = abbba,最少刷两遍,第一遍刷aaaaa,第二遍刷bbb;去掉头变为bbba,也要刷两遍;去掉尾变abbb,也要刷两遍。

B[i] != B[j],用标准的区间操作,把区间分为[i,k][k+1,j]两部分来dp

  1. 从A转换到B

A[j] == B[j],不用转换,有dp[1][j] = dp[1][j-1]

A[j] != B[j],用标准的区间操作,把区间分为[i,k][k+1,j]两部分来dp;这里利用了从空白串转化为B的结果,当区间[k+1,j]内的A和B字符不同时,从A转化到B与从空白串转化到B是一样的

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;

int dp[102][102];
string a,b;

void solve(const int& n){
	for(int len=2;len<=n;len++){
		for(int i=1;i<=n-len+1;i++){
			int j = i+len-1;
			dp[i][j] = inf;
			if(b[i] == b[j])
				dp[i][j] = dp[i+1][j];
			else for(int k = i;k < j;k++)
				dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j]);
		}
	}
	for(int j=1;j<=n;j++){
		if(a[j] == b[j])
			dp[1][j] = dp[1][j-1];
		else for(int k=1;k<j;k++)
			dp[1][j] = min(dp[1][j], dp[1][k] + dp[k+1][j]);
	}
	cout << dp[1][n] << '\n';
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	while(cin >> a && cin >> b){
		int n = a.size();	
		a = ' ' + a;
		b = ' ' + b;
		for(int i=1;i<=n;i++) dp[i][i] = 1;
		solve(n);
	}
	return 0;
}

洛谷P2858

上周的神孙权那道题和这个很像,都是每次从头、尾中的一个开始摸。这道题为为每一个摸的数字加了权

区间dp状态转移方程:
dp[i][j] = max(dp[i+1][j] + a[i]*(n-len+1), dp[i][l-1] + a[j]*(n-len+1))

也就是从小区间的最优解递推到大区间

有个坑:初始化每个长度为1的区间的值为a[i]*n,因为dp是从长度为2的区间开始状态转移的,而长度为2的区间由长度为1的区间转移过来,长度为1就表示这个数是最后一天摸的,因此初始化要*n

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e3+5;

int dp[maxn][maxn],a[maxn];

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int n;
	cin >> n;
	for(int i=1;i<=n;i++) cin >> a[i];
	for(int i=1;i<=n;i++) dp[i][i] = a[i]*n;
	for(int len = 2;len<=n;len++){
		for(int i = 1;i<=n-len+1;i++){
			int j = i+len-1;
			dp[i][j] = max(dp[i][j-1] + a[j]*(n-len+1), dp[i+1][j] + a[i]*(n-len+1));
		}
	}
	cout << dp[1][n];
	return 0;
}

加分二叉树

又是树,又是dp,咋一看是树形dp?

其实不然,题目的这句话“设一个 n 个节点的二叉树 tree 的中序遍历为(1,2,3,…,n)”,就表面这道题用区间dp来做

定义状态dp[i][j]是以区间[i,j]为树的最大分数

状态转移方程为

dp[i][j] = max(dp[i][j], dp[i][k-1]*dp[k+1][j] + w[k])

不过这里不能直接死板地转移状态,因为题目要求前序遍历输出树

所以要用到一个dp常用策略:存储每一个时刻的最优决策

这里用root[i][j]数组来存储区间[i,j]的最大值对应的根节点

然后dfs前序遍历输出树

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 35;

int dp[maxn][maxn], w[maxn], root[maxn][maxn];

void dfs(int l,int r){
	if(l > r) return;
	int mid = root[l][r];
	cout << mid << '\n';
	dfs(l,mid-1);
	dfs(mid+1,r);
}

signed main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int n;
	cin >> n;
	for(int i=1;i<=n;i++) cin >> w[i];
	for(int i=1;i<=n;i++){
		dp[i][i] = w[i];
		dp[i][i-1] = 1;
		root[i][i] = i;
	}
	for(int len = 2;len <= n;len++){
		for(int i=1;i+len-1 <=n;i++){
			int j = i+len-1;
			for(int k = i;k<=j;k++){
				if(dp[i][j] < dp[i][k-1]*dp[k+1][j] + w[k]){
					dp[i][j] = dp[i][k-1] * dp[k+1][j] + w[k];
					root[i][j] = k;
				}
			}
		}
	}
	cout << dp[1][n] << '\n';
	dfs(1,n);
	return 0;
}

线性丢番图方程

方程$ ax + by = c \(称为二元线性丢番图方程,其中\) a、b、c \(为已知数,\) x、y $为未知量,问是否有整数解。

$ ax + by = c $实际上是二维x-y平面内的一条直线,这条直线上如果有整数点,那么方程就有解,且有无穷多个整数点(解);这条直线上没有整数点,那么方程就没有整数解。

$ ax + by = c \(有解的充分必要条件是\) d = \gcd(a, b) \(能整数\) c $

因为:

设$ a = d \cdot a' \(,\) b = d \cdot b' \(,其中\) \gcd(a', b') = 1 $

那么方程等价于

$ d(a'x + b'y) = c $

因此$ c \(必须是\) d $的整数倍才有解

如果$ (x_0, y_0) $是一个特解,那么所有解(通解)可以表示为

$
\begin{cases}
x = x_0 + \frac{b}{d} \cdot n \
y = y_0 - \frac{a}{d} \cdot n
\end{cases}
\quad \text{其中 } n \in \mathbb{Z} $

因为:

$ (x_0, y_0) \(是一个格点(整数解的点),移动到另一个格点\)
(x_0 + \Delta x ,y_0 + \Delta y)
$,

有$ a \Delta x + b \Delta y = 0 \(,最小的整数解为\) \Delta x = b/d \(,\) \Delta y = -a/d $

扩展欧几里得算法 与 二元丢番图方程的解

求解方程$ ax + by = c \(的关键是找到一个特解\) (x_0, y_0) $,

用扩展欧几里得算法求一个特解$ (x_0, y_0) $的代码如下:

//返回 d = gcd(a,b); 并返回 ax + by = c的特解x,y
typedef long long ll;
ll extend_gcd(ll a,ll b,ll &x,ll &y){
    if(b == 0){x = 1; y = 0; return a;}
    ll d = extend_gcd(b,a%b,y,x);
    y -= a/b * x;
    return d;
}

该算法很高效,为$ (O_{log\ min(a,b)}) $

例题--洛谷P1516青蛙的约会

可以发现,两只青蛙跳的路程取余L是同余的,也就是方程

$ x+km≡y+kn\ (mod\ L) $

$ k $即为所求。变化方程为

$ x+km−(y+kn)=Lz\ ,\ z∈Z $

再将其变化为

$ k(m-n) - tL=-(x+y) $

令$ W = (n-m) \(,\) S = (x-y) $

方程就变化为

$ kW + tL = S $

这就是一个二元方程了,要做的就是求出最小解$ k_{min} $

用exgcd解,方程就转化成

$ k_jW +t_jL = (W,L) $

求出的$ k_j $就是一个特解

所有解可以表示为

$ k_i=k_j+c\frac{L}{gcd(W,L)}
$

这个方程对于$ c∈Z $而言,想通过特解推出一个最小解,可以这样做

$ k_{min}=k_j\ mod\ \frac{L}{gcd(W,L)} $

而因为这个$ k \(<font style="color:rgb(64, 64, 64);">是建立在 exgcd 得出的方程上的,方程右边是</font>\) gcd(W,L) \(<font style="color:rgb(64, 64, 64);">而不是</font>\) S \(<font style="color:rgb(64, 64, 64);">。所以最后我们需要将结果</font>\) ×\frac{S}{gcd(W,L)} $

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll exgcd(ll a,ll b,ll &x,ll &y){
	if(b == 0) {x = 1; y = 0; return a;}
	int d = exgcd(b,a%b,y,x);
	y -= a/b *x;
	return d;
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	ll x,y,m,n,l;
	cin >> x >> y >> m >> n >> l;
	ll k,t, b = n-m, a = x-y;
	if(b < 0){
		b = -b;
		a = -a;
	}
	int d = exgcd(abs(m-n),l,k,t);
	if(a % d !=0) cout << "Impossible\n";
	else cout << (k*a/d % (l/d) + (l/d))%(l/d) << '\n';
	return 0;
}

裴蜀定理(贝祖定理)

如果$ a \(与\) b \(均为整数,则有整数\) x \(和\) y \(使得\) ax+by=gcd(a,b) $

推论

整数$ a \(与\) b \(互素当且仅当存在整数\) x \(和\) y \(,使得\) ax+by=1 $

一道例题--洛谷P4549

n = 1时,答案就是输入的数

n > 1时,先看前两个数$ A_1X_1+A_2X_2=C \(,由于\) d = gcd(A_1,A_2)\ | \ C \(,显然这里\) S \(取\) gcd(A_1,A_2) $时最小;后面的数和前面的结果进行gcd即可

注意,当$ A_i<0 $时,gcd可能会返回负数,因此最后一步输出正数即可

#include <bits/stdc++.h>
using namespace std;

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int n,num1;
	cin >> n >> num1;
	for(int i=2;i<=n;i++){
		int num2;
		cin >> num2;
		num1 = __gcd(num1,num2);
	}
	cout << (num1>0 ? num1:-num1) << '\n';
	return 0;
}

HUD5512 ICPC亚洲区沈阳站2015

问题可转化为线性方程组$ ax + by = c $的解的个数减去2

设解的个数为$ P \(,显然,\) P $为奇数时Yuwgna胜,为偶数时Iaka胜

运用裴祖定理,

由于$ gcd(a,b) \ | \ c \(,因此解的个数就是\) gcd(a,b) \(的倍数个,\) P = n / gcd(a,b) $

#include <bits/stdc++.h>
using namespace std;

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int t,cnt = 1;
	cin >> t;
	while(t--){
		int n,a,b;
		cin >> n >> a >> b;
		int p = n/__gcd(a,b)-2;
		cout << "Case #" << cnt++ << ": "<< (p&1 ? "Yuwgna\n" : "Iaka\n"); 
	}
	return 0;
}

pick定理

在一个平面直角坐标系内,如果一个多边形的顶点都是格点,多边形的面积等于边界上格点数的一半加上内部的格点数减1,即$ s = j/2 + k - 1 $

同余

$ a≡b\ (mod\ m) \(称为\) a \(和\) b \(模\) m $同余

这里有$ m\ |\ (a-b) $

同余式转等式

若$ a \(和\) b \(是整数,\) m \(是正整数,则\) a≡b\ (mod\ m) \(当且仅当存在整数\) k \(,使得\) a=b+km $。

例如$ 19≡-2(mod\ 7) \(,有\) 19 = -2+3×7 $

模运算性质

(a + b) %p = (a%p + b%p) %p

(a * b) %p = (a%p * b%p) %p

这是加法和乘法的模运算。对于除法,也能满足

(a / b) %p = (a%p / b%p) %p吗?

不满足!举反例a=8,b=2,p=6就不满足

逆Inverse

求解一般形式的同余方程$ ax≡b\ (mod\ m) $,需要用到逆(Inverse)

给定整数$ a \(,且满足\) gcd(a,m) = 1 \(,称\) ax≡1(mod\ m) \(的一个解为\) a \(模\) m \(的逆,记为\) a^{-1} $。

例如,$ 8x≡1\ (mod\ 31) $,丢番图方程:

$ 8x+31y=1 $

有一个解是$ x = 4 $,4是8模31的逆,因为4×8-1才能整除31。所有解,如35,66等,都是8模31的逆

扩展欧几里得算法 求单个逆

例题洛谷P1082,求解同余方程$ ax≡1\ (mod\ m) $

$ ax≡1\ (mod\ m) \(,即丢番图方程\) ax +my=1 \(,用扩展欧几里得算法求出一个特解\) x_0 \(,通解就是\) x = x_0 + tm \(。再用\) ((x_0\ mod \ m)+m)\ mod\ m $求出最小正整数解

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

//先用扩展欧几里得方法求出特解 
ll exgcd(ll a,ll b,ll &x,ll &y){
	if(b == 0) {x = 1; y = 0; return a;}
	ll d = exgcd(b,a%b,y,x);
	y -= a/b * x;
	return d;
}

ll inv(ll a,ll m){
	ll x,y;
	exgcd(a,m,x,y);
	return (x%m + m) % m; //因为要最小正整数 
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	ll a,b;
	cin >> a >> b;
	cout << inv(a,b);
	return 0;
}

百度之星2024题集

BD202401

第一眼以为是背包,题读完了发现贪个心完事

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3+5;
using pii = pair<int,int>;

pii a[maxn];

bool cpa(pii a,pii b){
	return (a.first+a.second) < (b.first+b.second);
}

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int n,b;
	cin >> n >> b;
	for(int i=1;i<=n;i++){
		int p,s;
		cin >> p >> s;
		a[i] = {p,s};
	}
	sort(a+1,a+n+1,cpa);
	for(int i=1;i<=n;i++){
		if(b - a[i].first - a[i].second >= 0)
			b -= a[i].first+a[i].second;
		else if(b - a[i].first/2 - a[i].second >= 0){
			cout << i << '\n';
			break;
		}
		else{
			cout << i-1 << '\n';
			break;
		}
	}
	return 0;
}

A


签到。自己写几组数据可以发现,直接输出最大的数就行。

贪心地想,自己&自己是最优解,在每个数中,最大数按位与出来的结果是最大的

#include <bits/stdc++.h>
using namespace std;

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int t;
	cin >> t;
	while(t--){
		int n,ans = 0;
		cin >> n;
		for(int i = 1;i<=n;i++){
			int cache;
			cin >> cache;
			ans = max(ans, cache);
		}
		cout << ans << '\n';
	}
	return 0;
}

B

打表--查表

#include <bits/stdc++.h>
using namespace std;

map <char,string> ma;

int main(void){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	ma['0'] = "---";
	ma['1'] = "--x";
	ma['2'] = "-w-";
	ma['3'] = "-wx";
	ma['4'] = "r--";
	ma['5'] = "r-x";
	ma['6'] = "rw-";
	ma['7'] = "rwx";
	int t;
	cin >> t;
	while(t--){
		string s;
		cin >> s;
		for(char i:s) cout << ma[i];
		cout << '\n';
	}
	return 0;
}

[墓碑]题目已丢失

emmm队友发来一道数据结构,随手做了做,题目没保存下来......

立个墓碑

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
map <int,int> ma;

int sec[maxn]{};

inline void inser(int num) {ma[num]++;}
inline void print() {cout << ma.begin()->first << '\n';}
inline void era() {ma.erase(ma.begin()->first);}
inline void era(int num) {ma[num]--;if(ma[num] == 0) ma.erase(num);}
inline void xiu(int n1,int n2) {ma[n1]--;ma[n2]++;if(ma[n1] == 0) ma.erase(n1);}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int n, cnt = 0;
	cin >> n;
	for(int i=1;i<=n;i++){
		string s;
		cin >> s;
		if(s[0] == 'I'){
			int num;
			cin >> num;
			inser(num);
			sec[++cnt] = num;
		}
		else if(s[0] == 'P') print();
		else if(s[0] == 'D' && s.size() == 2) era();
		else if(s[0] == 'D'){
			int k;
			cin >> k;
			era(sec[k]);
		}
		else{
			int k,x;
			cin >> k >> x;
			xiu(sec[k],x);
		}
		
	}
	return 0;
}
posted @ 2025-06-14 12:28  HLAIA  阅读(35)  评论(0)    收藏  举报