海亮 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;
}