[WC2022] 杂题选讲-邓明扬
stars
题目描述
一颗星星可以抽象成 \(k\) 维空间中的一个整点。称若干星星构成的集合 \(s\) 是奇妙的,当且仅当存在 \(k\) 维空间中的整点 \(P\),\(P\) 与 \(s\) 中的每颗星星至少有一维坐标相同。
有一个长度为 \(n\) 的星星序列 \(A\) ,请你求出所有奇妙子区间的个数之和。
\(1\leq n\leq 10^5,1\leq k\leq 5\)
解法
首先考虑如何判定序列 \(s\) 是奇妙的,由于 \(k\) 很小我们可以直接使用枚举法。由于每个点都需要被覆盖,所以我们可以考虑枚举如何解决掉序列 \(s\) 的第一个点,其实就是枚举 \(P\) 的哪个位置用于解决它,然后往后推看有多少点顺带被解决了,遇到下一个不能解决的点再枚举 \(P\) 的一个位置。
不难发现只需要枚举长度为 \(k\) 的所有排列就可以判定合法性,但是暴力判定还是不可行,我们考虑用 \(dp\) 来优化这个过程,由于排列非常小我们可以直接塞状态里面。设 \(dp[i][s]\) 表示后 \(i\) 个点用位置排列 \(s\) 的最远延伸距离(其中 \(s\) 的单个元素被邓老师形象地称为锦囊),考虑如何转移。
写出转移需要强大的观察能力,这里邓老师观察出了问题之间的相似性。我们考虑 \(dp[i][s]\) 和 \(dp[i+1][s']\)(其中 \(s'\) 表示 \(s\) 去掉第一个锦囊之后的位置排列,设其为 \(x_0\)),只是对于 \(dp[i+1][s']\) 第一个需要新增锦囊但是可以被 \(x_0\) 解决的位置,是不需要再新增锦囊的。那么我们可以让 \(x_0\) 去解决它,所以我们把 \(x_0\) 插入到当前最后一个锦囊的下一个位置就得到了 \(i+1\) 的等效子问题,也就是对于以后的影响都等效地传递下去了。
实现小细节:\(dp[i][s]\) 开成一个 \(k\) 维结构体,每一维记录使用前 \(i\) 个锦囊获得的最远延伸距离。
总结
寻找子问题的关键:观察问题之间的相似性。
如果某个元素对于以后的影响是长远的,但又只能考虑一小步转移,那么寻找当前问题的等效子问题,就可以把这个影响传递下去,完成转移。
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 100005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,cnt,a[M][5],b[M];long long ans=1;
struct node
{
int p[5];
int &operator[](int x) {return p[x];}
int Hash()
{
int hs=0;
for(int i=0;i<m;i++) hs=hs*5+p[i];
return hs;
}
}w,dp[2][125];
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
for(int j=0;j<m;j++) a[i][j]=read();
for(int i=0;i<m;i++) w.p[i]=i;
do {b[w.Hash()]=++cnt;}
while(next_permutation(w.p,w.p+m));
for(int i=0;i<=120;i++)
for(int j=0;j<m;j++) dp[n&1][i][j]=n+1;
for(int i=n-1;i>=1;i--)
{
int mx=0,o=(i+1)&1;cnt=0;
for(int j=0;j<m;j++) w.p[j]=j;
do
{
cnt++;
if(a[i][w[0]]==a[i+1][w[0]])
{
dp[i&1][cnt]=dp[o][cnt];
mx=max(mx,dp[o][cnt][m-1]);
continue;
}
node t;int p=m;
for(int j=0;j<m;j++) t[j]=w[(j+1)%m];
node nw=dp[o][b[t.Hash()]];
for(int j=0;j<m-1;j++)
{
if(a[i][w[0]]==a[nw[j]][w[0]])
{
for(int k=m-1;k>=j+1;k--)
t[k]=t[k-1];
t[j+1]=w[0];p=j+1;break;
}
}
dp[i&1][cnt]=dp[o][b[t.Hash()]];
for(int j=p-1;j;j--)
dp[i&1][cnt][j]=dp[i&1][cnt][j-1];
dp[i&1][cnt][0]=i+1;
mx=max(mx,dp[i&1][cnt][m-1]);
}while(next_permutation(w.p,w.p+m));
ans+=mx-i;
}
printf("%lld\n",ans);
}
Od deski do deski
题目描述
有 \(n\) 棵树,每棵树可能是 \(m\) 中之一。小 \(C\) 每天可以选择连续的一段树砍掉,要求这一段的长度至少是 \(2\),并且第一棵与最后一棵的种类相同。问有多少种初始局面,使得存在一种方式把树砍完,答案对 \(10^9+7\) 取模。
\(n\leq 3000,m\leq 10^9\)
解法
首先还是考虑如何判定,显然的思路是设 \(dp[i]\) 表示砍完前 \(i\) 棵树是否可行,那么转移就枚举 \(1\leq j<i\),当 \(dp[j-1]=1\and a[i]=a[j]\) 都成立的时候 \(dp[i]=1\)
考虑方案的区分其实就是靠 \(a\),所以计数的方法是考虑当前位置可以填上多少个不同的 \(a[i]\),这取决于满足 \(dp[j-1]=1\) 不同的 \(a[j]\) 数量。
那么我们尝试在状态中记录这个数量然后 \(dp\),还要保证填入 \(a[i]\) 之后这个数量可以被更新。设 \(dp[i][j][0/1]\) 表示考虑前 \(i\) 个位置,其中有 \(j\) 个不同的满足条件的 \(a\),\(dp[i]=0/1\) 的不同初始序列总数,我们枚举上一个状态 \(dp[i-1][j][k]\),那么转移:
时间复杂度 \(O(n^2)\)
#include <cstdio>
#define int long long
const int M = 3005;
const int MOD = 1e9+7;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,dp[M][M][2];
void add(int &x,int y) {x=(x+y)%MOD;}
signed main()
{
n=read();m=read();
dp[0][0][1]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<=i;j++)
for(int k=0;k<2;k++)
{
add(dp[i][j][1],dp[i-1][j][k]*j%MOD);
add(dp[i][j+k][0],dp[i-1][j][k]*(m-j)%MOD);
}
for(int j=0;j<=n;j++)
add(ans,dp[n][j][1]);
printf("%lld\n",ans);
}
Robbery
题目描述
有 \(n\) 种物品,第 \(i\) 种质量为 \(i\),价格为 \(a_i\),每种物品的数量无限。给定 \(k,w\),请你选择 \(k\) 个物品,满足质量总和是 \(w\),价值之和最大,请求出最大的价值之和。
\(n\leq 1000,k\leq 10^6,k\leq w\leq kn,1\leq a_i\leq 10^9\)
解法
由于物品的质量取值范围是 \([1,n]\),那么有一个经典的结论:存在方法把最优选取的物品分成两部分,两部分的差值小于等于 \(n\),这个结论为我们提供了分治的可能。
设 \(dp[k][w]\) 表示 \(k\) 个物品,权值总和为 \(w\) 的价值最大值,可以写出暴力的转移:
我们考虑状态总数,因为 \(\frac{w}{2}\) 的操作,所以每一层的个数大概是 \(n+\frac{n}{2}+\frac{n}{4}....=O(n)\),一共有 \(O(\log k)\) 层。转移的复杂度是 \(O(n)\) 的,所以总复杂度 \(O(n^2\log k)\)
小细节:别像我一样乱写 \(\tt map\) 超时了,可以预处理出每一层的标准长度,那么状态一定是在标准长度上下浮动的,所以用和标准长度的差值加上一个常量开数组就可以了。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 1005;
const int N = 1000005;
#define int long long
const int inf = 1e18;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,k,w,a[M],ind[N],tot[N],dp[50][M<<3];
int f(int k,int w)
{
if(w<k || w>n*k) return -inf;
if(!k) return 0;
int &t=dp[ind[k]][w-tot[k]+2500];
if(t!=-1) return t;
if(k%2)
{
for(int i=1;i<=n;i++)
t=max(t,f(k-1,w-i)+a[i]);
}
else
{
for(int i=-n/2;i<=n/2;i++)
t=max(t,f(k/2,w/2-i)+f(k/2,(w+1)/2+i));
}
return t;
}
signed main()
{c++
n=read();k=read();w=read();tot[k]=w;
memset(dp,-1,sizeof dp);
for(int i=1;i<=n;i++) a[i]=read();
for(int i=k,cnt=0;i>=1;)
{
if(i&1) ind[i]=++cnt,tot[i-1]=tot[i],i--;
else ind[i]=++cnt,tot[i/2]=tot[i]/2,i>>=1;
}
printf("%lld\n",f(k,w));
}
Poborcy podatkowi
题目描述
有一棵 \(n\) 个点的树,边有边权(可能为负数),请你找出一些不交的长度为 \(4\) 的路径,使得权值之和最大,输出权值之和的最大值。
\(n\leq 2\cdot 10^5\)
解法
可以直接树形 \(dp\),设 \(dp[u][0/1/2/3]\) 表示 \(u\) 为根留给父亲的链长度是 \(0/1/2/3\) 的最大权值。合并的儿子的时候考虑必须要有偶数个 \(dp[v][1]\)(长度为 \(2\) 的链),\(dp[v][0],dp[v][2]\) 的数量必须相等(凑出长度为 \(4\) 的链),\(dp[v][3]\) 可以直接并上来。
根据上面的讨论,我们设计 \(g[i][j]\) 表示长度为 \(2\) 的链奇偶性是 \(i\),长度为 \(1\) 的链和长度为 \(3\) 的链之差是 \(j\),那么转移就类似于做一个背包,根据经典结论我们可以把儿子序列 \(\tt random\_shuffle\) 一下,第二维就可以限制在 \(\sqrt n\) 了:
当然上面的东西我是看不懂的,时间复杂度 \(O(n\sqrt n)\)
#include <cstdio>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <ctime>
using namespace std;
const int M = 200005;
const int N = 450;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,dp[M][4];
struct edge{int v,c;};vector<edge> g[M];
void upd(int &x,int y) {x=max(x,y);}
void dfs(int u,int fa)
{
int pl[2][2][N*2+10]={};
memset(pl,-0x3f,sizeof pl);
int cnt=0,w=0,*f[2][2]=
{pl[0][0]+N+5,pl[0][1]+N+5,
pl[1][0]+N+5,pl[1][1]+N+5};
//
random_shuffle(g[u].begin(),g[u].end());
for(auto x:g[u]) if(x.v!=fa) dfs(x.v,u);
//
int mi=f[0][0][0];f[0][0][0]=0;
for(auto x:g[u]) if(x.v^fa)
{
int v=x.v,c=x.c;
for(int i=0;i<2;i++)
for(int j=-cnt;j<=cnt;j++)
{
int tmp=f[w][i][j];if(tmp<=mi) continue;
upd(f[!w][i][j],tmp+dp[v][0]);//do nothing
upd(f[!w][i][j],tmp+c+dp[v][3]);//3+(1)
upd(f[!w][!i][j],tmp+c+dp[v][1]);//2+2
if(j>-N) upd(f[!w][i][j-1],tmp+c+dp[v][2]);
if(j<N) upd(f[!w][i][j+1],tmp+c+dp[v][0]);
}
w^=1;if(cnt<N) cnt++;
}
dp[u][0]=f[w][0][0];
dp[u][1]=f[w][0][1];
dp[u][2]=f[w][1][0];
dp[u][3]=f[w][0][-1];
}
signed main()
{
srand(time(0));n=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read(),c=read();
g[u].push_back(edge{v,c});
g[v].push_back(edge{u,c});
}
dfs(1,0);
printf("%lld\n",dp[1][0]);
}