海亮 7.12 dp专题1

海亮 7.12 dp专题1

P6280 [USACO20OPEN] Exercise G

我们可以发现 一个排列是由很多循环节构成的 那么这些循环节的长度取\(lcm\)即为一个合法的\(k\)

问题转化为对于\(n\)\(n\)拆成\(\sum len_i\)\(lcm\{len_i\}\) 的所有可能取值之和

结论:根据算术唯一分解定理 \(n\)可以被分解为\(\sum_{i=1}^m p_i^{c_i}\) 那么\(len_i=p_i^{c_i}\)

证明:假设存在一个\(len\)不符合上述分解 那么根据算术唯一分解 显然可以将这个\(len\)分解成几个\(p_i^{c_i}\) 而且对于长度没有影响 那么可以继续分解 所以不存在这类的\(len\)

那么我们设置\(f[i][j]\)为枚举到质数\(i\) 因数和为\(j\)\(k\)的和

状态转移即为\(f[i][j]=f[i-1][j-k]*k\) (\(k\)\(i\)的所有次方 因为每一个\(k\)都会被当做最高次幂对\(lcm\)做出贡献 当然这个\(k\)要满足\(k\le j\))

显然\(i\)可以滚掉

那么最后的答案就是\(\sum_{i=0}^nf[i]\) 因为\(f[0]\)也有一种\(1-n\)全排列的情况

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e4 + 5; 
inline int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int f[N] , n , mod;//前i个质数 和为j的lcm和 
int isntprime[N] , prime[N] , cnt , ans;

signed main ()
{
	ios::sync_with_stdio(false);
	n = read() , mod = read();
	for ( int i = 2 ; i <= n ; i ++ )
	{
		if ( !isntprime[i] ) prime[++cnt] = i;
		for ( int j = 1 ; j <= cnt && i * prime[j] <= n ; j ++ )
		{
			isntprime[prime[j]*i] = 1;
			if ( i % prime[j] == 0 ) break;	
		}
	}
	f[0] = 1;//和为0的时候也有一种情况 为1-n的排列
	for ( int i = 1 ; i <= cnt ; i ++ )
		for ( int j = n ; j >= prime[i] ; j -- )
		{
			int k = prime[i];		
			while ( k <= j ) f[j] = ( f[j] + ( f[j-k] * k ) ) % mod , k *= prime[i];
		}
	for ( int i = 0 ; i <= n ; i ++ ) ans = ( ans + f[i] ) % mod;
	cout << ans;
	return 0;
}

Karen and Supermarket

一个常规思路是设置\(f[u][i][0/1]\)表示在\(u\)的子树中 强制取\(u\)点 价值为\(i\) 使用优惠券与否的最小花费

不过这里的\(b\)值域太大了 所以假了()

所以令\(f[u][i][0/1]\)表示以\(u\)为根的子树 购买\(i\)件商品 \(u\)是否使用优惠券的最小花费

那么转移

\(f[u][j+k][0] = min ( f[u][j+k][0] , f[u][j][0] + f[v][k][0] )\)

\(f[u][j+k][1] = min ( f[u][j+k][1] , f[u][j][1] + min ( f[v][k][1] , f[v][k][0] ) )\)

\(j\)的枚举顺序必须为倒序 因为我们考虑当前的\(v\)节点 它是在枚举上一个\(v\)节点之后被枚举到的

相当于\(i\)节点在\(i-1\)节点后 相当于我们压掉了一个维度 那么和\(01\)背包一样倒序枚举即可

\(但\)k$的枚举无所谓 倒序和正序都可

注意\(u\)的上界和\(v\)的上界要注意 否则会从\(O(n^2)\)退化为\(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5000 + 5; 
inline int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , val[N] , dis[N] , sz[N] , f[N][N][2];

int head[N] , cnt;
struct DQY { int to , nxt; } e[N<<1];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }

void dfs ( int u )
{
	sz[u] = 1;
	f[u][0][0] = 0;
	f[u][1][0] = val[u];
	f[u][1][1] = val[u] - dis[u];
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		dfs(v);
		for ( int j = sz[u] ; j >= 0 ; j -- )
			for ( int k = sz[v] ; k >= 0 ; k -- )
			{
				f[u][j+k][0] = min ( f[u][j+k][0] , f[u][j][0] + f[v][k][0] );
				f[u][j+k][1] = min ( f[u][j+k][1] , f[u][j][1] + min ( f[v][k][1] , f[v][k][0] ) );
			}
		sz[u] += sz[v];
	}
}

signed main ()
{
	ios::sync_with_stdio(false);
	n = read() , m = read();
	val[1] = read() , dis[1] = read();
	for ( int i = 2 , fa ; i <= n ; i ++ ) val[i] = read() , dis[i] = read() , fa = read() , add ( fa , i );
	memset ( f , 0x3f , sizeof f );
	dfs(1);
	for ( int i = n ; i ; i -- )
		if ( f[1][i][0] <= m || f[1][i][1] <= m )
			{ cout << i << endl; return 0; }
	cout << 0 << endl;
	return 0;
}

P6147 [USACO20FEB] Delegation G

我们可以发现 经过点\(u\)的链要么是一条祖先到儿子的链 要么是这个节点作为\(lca\)的链

那么显然对于某个点\(u\) 合法的第一种链只有一条

那么我们可以对于每一个点\(u\) 维护和它配对的子链列表 那么如果只有一条子链无法配对 那么是合法的

\(multiset\)代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5; 
inline int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n;

int head[N] , cnt;
struct DQY { int to , nxt; } e[N<<1];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }

int dfs ( int u , int f , int k )
{
	multiset<int>s;
	for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt )
	{
		if ( f == v ) continue;
		int res = dfs ( v , u , k );
		if ( res == -1 ) return -1;
		if ( res == k ) continue;
		if ( s.count(k-res) ) s.erase(s.find(k-res));
		else s.insert(res);
	}
 	if ( s.size() == 0 ) return 1;
 	if ( s.size() == 1 ) return *s.begin() + 1;	
 	return -1;
} 


signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
	for ( int i = 1 ; i < n ; i ++ )
	{
		if ( ( n - 1 ) % i ) cout.put('0');//只有可以被整除的才有机会 
		else
		{
			if ( dfs ( 1 , 0 , i ) == 1 ) cout.put('1');
			else cout.put('0');
		}
	}
	return 0;
}

\(map\)代码:(\(30\%\ copied\ from\ studying\)\(father\))

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5; 
inline int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , f[N] , sz[N];

int head[N] , cnt;
struct DQY { int to , nxt; } e[N<<1];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }

int dfs ( int u , int f , int k )
{
	map<int,int> mp;
	sz[u] ++; //代表的是这个节点的剩余链大小(也可能没有剩余) 
	for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt )
	{
		if ( f == v ) continue;
		if ( ! dfs ( v , u , k ) ) return 0;
		int match = k - sz[v];
		if ( mp.count(match) )
		{
			mp[match] -- , sz[u] -= match;
			if ( mp[match] == 0 ) mp.erase(match);//如果没有值了 那么将这个值清理掉 
		}
		else if ( k != sz[v] ) sz[u] += sz[v] , mp[sz[v]] ++;//这棵子树的链加入失配队列 
	}
	int rem = 0;
	for ( auto it : mp ) rem += it.second;
	return rem <= 1;//如果不能配对的小于一条 那么合法 
} 


signed main ()
{
	ios::sync_with_stdio(false);
	n = read();
	for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
	for ( int i = 1 ; i < n ; i ++ )
	{
		if ( ( n - 1 ) % i ) cout << 0;//只有可以被整除的才有机会 
		else
		{
			for ( int i = 1 ; i <= n ; i ++ ) sz[i] = 0;
			if ( dfs ( 1 , 0 , i ) ) cout << 1;
			else cout << 0;
		}
	}
	return 0;
}

P3959 [NOIP2017 提高组] 宝藏

正解是状压(虽然我不会) 但是可以暴搜

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
const int N = 1e3 + 5;
const int inf = 1000000;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , cntnode[N] , lay[N][N] , mp[N][N] , ans = inf , use[N];//cntnode是这一层有几个点  lay[i][j]表示第i层第j个点是什么 

void dfs ( int layer , int p , int cnt , int res )//当前进行到了第layer层 当前在讨论p节点 已经加入了cnt个节点 现在的价值是res 
{
	if ( res > ans ) return;//最优 
	if ( cntnode[layer-1] == 0 ) return;
	if ( cnt == n ) return ans = min ( ans , res ) , void();
	if ( p > n ) return dfs ( layer + 1 , 1 , cnt , res ) , void();//下一层 
	if ( use[p] == 1 ) return dfs ( layer , p + 1 , cnt , res ) , void();//这个点被访问过了 跳过 
	use[p] = 1; int minn = inf;
	for ( int i = 1 ; i <= cntnode[layer-1] ; i ++ ) minn = min ( minn , mp[p][lay[layer-1][i]] );//减一!!!!!!!!!!!! 
	use[p] = 1;
	lay[layer][++cntnode[layer]] = p;
	dfs ( layer , p + 1 , cnt + 1 , res + minn * layer );
	--cntnode[layer];
	use[p] = 0;
	dfs ( layer , p + 1 , cnt , res );
}
 

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read();
	memset ( mp , 0x3f , sizeof mp );
	for ( int i = 1 , u , v , w ; i <= m ; i ++ ) { u = read() , v = read() , w = read(); if ( w < mp[u][v] ) mp[u][v] = mp[v][u] = w; }
	for ( int i = 1 ; i <= n ; i ++ )
	{
		use[i] = 1;
		lay[0][++cntnode[0]] = i;
		dfs ( 1 , 1 , 1 , 0 );
		--cntnode[0];
		use[i] = 0;	
	}
	cout << ans << endl;	
	return 0;
}

Tree Elimination

因为删除的时候边有先后顺序,这题 \(dp\) 的状态和转移基于时间

状态设置( \(x\) 是什么时候被删除的?或者没被删除?):\(f_{x,0/1/2/3}\) 分别表示:点 \(x\) 被父亲边之前的边删除、点 \(x\) 被父亲边删除、点 \(x\) 被父亲边之后的边删除、点 \(x\) 没有被删除 四种情况下 \(x\) 子树内的答案。

转移:

对于 \(f_{x,0/2}\)\(x\) 不是被父亲边删除,那一定是被和儿子相连的边删除。枚举这个儿子 \(y\)\(y\) 此时还没被删,取 \(f_{y,2/3}\)。对于其他儿子 \(z\),按照时间分类:

1、\((x,z)\)\((x,y)\) 前,记为 \(z<y\)。如果这时候 \(z\) 还在,说明 \(x\) 已经没了,不符合情况。所以取 \(f_{z,0/1}\)

2、\((x,z)\)\((x,y)\) 后,记为 \(z > y\) 。后面的时间点对当前不影响,但不能被父亲边删,因为 \(x\) 已经挂了,取 \(f_{z,0/2/3}\)

\(f_{x,0/2}=\sum\limits_{y\in son(x),y<fa_x(y>fa_x)}(f_{y,2/3}\times\prod\limits_{z<y}f_{z,0/1}\times \prod\limits_{z>y}f_{z,0/2/3})\)

注:上式中对于 \(f_{x,0}\)\(f_{x,2}\) 所选的 \(y\) 条件不同,即在父亲边之前和父亲边之后

对于 \(f_{x,1}\)

1、所有 \(y<fa_x\) 已经不在(如果在,\(x\) 就挂了),并且 \((x,y)\) 这条边不可能删除 \(x\),所以 \(y\) 的删除不会拖到 \((x,y)\) 之后,不能取 \(f_{y,2}\),只能取 \(f_{y,0/1}\)

2、\(y>fa_x\),还是随便 \(y\) 怎么样,但因为 \((x,y)\) 这条边还没弄,所以不能取 \(f_{y,1}\),剩下的 \(f_{y,0/2/3}\) 均可

\(f_{x,1}=\prod\limits_{y<fa_x}f_{y,0/1}\times\prod\limits_{y>fa_x}f_{y,0/2/3}\)

对于 \(f_{x,3}\),所有儿子 \(y\) 肯定都挂了,并且不会拖到 \((x,y)\) 以后。取 \(f_{y,0/1}\)

这里的\(x,3\)状态是"\(dp\)到当前节点就能判断出来\(x\)未被覆盖"的状态 儿子边如果晚 那么在当前状态中显然\(x\)不一定被覆盖 所以不能取得\((x,y)\)之后的边

\(f_{x,3}=\prod\limits_{y\in son(x)}f_{y,0/1}\)

那么我们做一个\(f[v][0]+f[v][2]+f[v][3]\)的后缀积\(c[i]\)\(f[v][0]+f[v][1]\)的前缀积\(a[i]\)即可转移

注意乘的时候需要时刻取模!!!

(其实这是一篇\(luogu\)的现成题解 加了一点点东西)

原文链接

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long 
const int N = 2e5 + 5;
const int mod = 998244353;
int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}
		
int f[N][4] , n , m , k;
		
int head[N] , cnt;
struct node { int to , nxt; } e[N<<1];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }
	
int a[N] , b[N] , c[N];
		
void dfs ( int u , int fa )
{
	vector<int> vv;
	for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt )
	{
		vv.push_back(v);
		if ( v != fa ) dfs ( v , u );
	} 
	m = k = 0;
	for ( int i = vv.size() - 1 ; i >= 0 ; i -- )
	{
		int v = vv[i];
		if ( v == fa ) k = m;
		else m ++ , a[m] = ( f[v][0] + f[v][1] ) % mod , b[m] = ( f[v][2] + f[v][3] ) % mod , c[m] = ( f[v][0] + f[v][2] + f[v][3] ) % mod; 
	}
	a[0] = 1 , c[m+1] = 1;
	for ( int i = m ; i ; i -- ) c[i] = c[i] * c[i+1] % mod;
	for ( int i = 1 ; i <= m ; i ++ ) a[i] = a[i] * a[i-1] % mod;
	for ( int i = 1 ; i <= m ; i ++ ) f[u][(i>k)<<1] = ( f[u][(i>k)<<1] + a[i-1] * b[i] % mod * c[i+1] % mod ) % mod;//内部三个东西的乘积也需要时刻取模!!! 
	f[u][1] = ( a[k] * c[k+1] ) % mod , f[u][3] = a[m] % mod;
}

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
	dfs ( 1 , 0 );
	cout << ( f[1][0] + f[1][2] + f[1][3] ) % mod << endl;	
	return 0;
}
posted @ 2023-07-12 18:34  Echo_Long  阅读(10)  评论(0编辑  收藏  举报