星期一:
白天做了俩道区间dp
其一: 洛谷传送门
思路:dp【i】【j】表示最左山为i,最右山为j的不对称性,由dp【i+1】【j-1】转化而来,先枚举区间长度,再枚举左端点
代码如下:
ll n;
int h[5050];
ll dp[5050][5050],ans[5050];
void solve(){
cin >> n;
memset(dp,0x3f,sizeof dp);
for(int i=2;i<=n;i++) ans[i]=1e9;
for(int i=1;i<=n;i++){
cin >> h[i];
dp[i][i]=0;
}
for(int i=1;i<n;i++)
dp[i][i+1]=abs(h[i+1]-h[i]),ans[2]=min(dp[i][i+1],ans[2]);
for(int len=3;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
dp[l][r]=min(dp[l+1][r-1]+abs(h[l]-h[r]),dp[l][r]);
ans[len]=min(dp[l][r],ans[len]);
}
}
for(int i=1;i<=n;i++) cout << ans[i] << " ";
}
其二: 洛谷传送门
思路:和上题很像,dp【i】【j】表示还剩 i到j零食时最多的钱,由dp【i-1】【j】或dp【i】【j+1】转化而来
代码如下:
ll n;
int v[2020],dp[2020][2020];
void solve(){
cin >> n;
for(int i=1;i<=n;i++) cin >> v[i];
for(int len=n-1;len>=1;len--){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1,a=n-len;
dp[l][r]=max({dp[l-1][r]+v[l-1]*a,dp[l][r+1]+v[r+1]*a,dp[l][r]});
}
}
ll ans=0;
for(int i=1;i<=n;i++) ans=max(1ll*dp[i][i]+v[i]*n,ans);
cout << ans;
}
下午补了道成信大校赛格温的剪刀: pta传送门
思路:二分+kk算法,和cf round 936的C有点像,开始可能会想成贪心,但贪心并不能得到最优解,于是得二分跑kk,边按照happy度排序,如果beaty度小于二分的值就跳过
代码如下:
const int N=2e6+10;
const int mod=1e9+7;
ll n;
int m;
struct nod{
int u,v,bea,hap;
}e[N];
int fa[N];
int fnd(int x){
return fa[x]==x?x:fa[x]=fnd(fa[x]);
}
ll check(ll x){
for(int i=1;i<=n;i++) fa[i]=i;
int cnt=0;ll sum=0;
for(int i=1;i<=m;i++){
if(e[i].bea<x) continue;
int u=fnd(e[i].u),v=fnd(e[i].v);
if(u==v) continue;
fa[v]=u;
cnt++;
sum+=e[i].hap;
if(cnt==n-1) return sum;
}
return 0;
}
bool cmp1(nod a,nod b){
return a.hap<b.hap;
}
void solve(){
cin >> n >> m;
for(int i=1;i<=m;i++)
cin >> e[i].u >> e[i].v >> e[i].bea >> e[i].hap;
sort(e+1,e+m+1,cmp1);
ll ans1=0,ans2=0;
ll l=1,r=INT_MAX;
while(l<=r){
ll mid=l+r>>1;
if(ll tmp=check(mid)) ans1=mid,ans2=tmp,l=mid+1;
else r=mid-1;
}
cout << ans1 << "\n" << ans2;
}
星期二:
上午做了道高精度like题,还有环形石子合并 洛谷传送门
思路:很典的化环为链,和线性石子合并基本一样
天体训练赛,遇到了道非常恶心的字符串题,没碰
还有一道倒着用并查集的题,不过我没想到,因为数据范围小,所以我正着每次操作后都跑了一遍图,倒着用并查集的trick要加深印象
区间dp 涂色 洛谷传送门
思路:dp【i】【j】表示将 i 到 j 涂好的操作数,考虑可以从什么状态转移过来
做了前两道区间dp我都忘了还能有第三重循环,dp【l】【r】可以从dp【l】【k】+dp【k+1】【r】转移过来,所以记得枚举l和r间的分界点,还有两个特判
代码如下:
ll n;
string s;
int dp[55][55];
void solve(){
cin >> s;
n=s.size(),s=" "+s;
memset(dp,0x3f,sizeof dp);
for(int i=1;i<=n;i++) dp[i][i]=1;
for(int len=2;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
if(s[l]==s[l+1] || s[l]==s[r]) dp[l][r]=min(dp[l+1][r],dp[l][r]);
if(s[r]==s[r-1] || s[r]==s[l]) dp[l][r]=min(dp[l][r-1],dp[l][r]);
for(int k=l;k<r;k++)
dp[l][r]=min(dp[l][k]+dp[k+1][r],dp[l][r]);
}
}
cout << dp[1][n];
}
星期三:
补了道集美大学15届校赛的背包问题: 牛客传送门
思路:因为总重量过大,不能跑常规的01背包,所以这里记dp【i】为装入 i 价值物品所需最小重量,状态转移为dp【i】=min(dp【i-v】+w,dp【i】),然后再跑遍dp数组确保其单调不减性,就可以对每次询问进行二分查询了
代码如下:
const int N=1e4+10;
const int mod=1e9+7;
ll n;
ll dp[N];
void solve(){
cin >> n;
int sumv=0;
memset(dp,0x3f,sizeof dp); //初始化
dp[0]=0;
for(int i=1;i<=n;i++){
int w,v; cin >> w >> v;
sumv+=v;
for(int j=sumv;j>=v;j--)
dp[j]=min(dp[j-v]+w,dp[j]);
}
for(int i=sumv-1;i;i--) if(dp[i]>dp[i+1]) dp[i]=dp[i+1];
int q; cin >> q;
while(q--){
ll w; cin >> w;
int l=1,r=sumv,res=0;
while(l<=r){
int mid=l+r>>1;
if(dp[mid]<=w) res=mid,l=mid+1;
else r=mid-1;
}
cout << res << "\n";
}
}
补道中传校赛的树题:
思路:对每个节点进行质因数分解,从根节点dfs,因子数量不够就往上传,够了就直接return 1,存质因数及其指数,用map来实现,最开始用的vector,难写及写拉了没过,map一下就过了
代码如下:
const int N=2e6+10,M=210;
const int mod=1e9+7;
ll n;
ll k;
int p[N],cnt;
bool vi[N];
vector<int>ve[N];
map<ll,ll>pi[N]; //二维map
ll ans;
void getp(int x){
for(int i=2;i<=x;i++){
if(!vi[i]) p[++cnt]=i;
for(int j=1;1ll*i*p[j]<=x;j++){
vi[i*p[j]]=1;
if(i%p[j]==0) break;
}
}
}
void calp(int x){
int y=x;
for(int i=1;p[i]<=x/p[i] && x!=1;i++){
int tmp=0;
while(x%p[i]==0){
x/=p[i];
tmp++;
}
if(tmp) pi[y][p[i]]+=tmp;
}
if(x!=1) pi[y][x]+=1;
}
void meg(int x,int y){
for(auto [a,b]:pi[y]) pi[x][a]+=b;
}
bool dfs(int x,int f){
bool if1=0;
for(auto i:ve[x]){
if(i==f) continue;
if(dfs(i,x)) if1=1;
else meg(x,i);
}
if(if1){ans++; return 1;}
ll sum=1;
for(auto [a,b]:pi[x]){
sum*=b+1;
if(sum>=k){ans++; return 1;}
}
return 0;
}
void solve(){
cin >> n >> k;
getp(n);
for(int i=1;i<n;i++){
int u,v; cin >> u >> v;
ve[u].push_back(v);
ve[v].push_back(u);
if(pi[u].empty()) calp(u);
if(pi[v].empty()) calp(v);
}
dfs(1,0);
cout << ans << "\n";
}
星期四:
牢王拉了套dp专题题单,开始板刷dp!
线性dp第一题:拼字符串 cf传送门
思路:dp【i】【j】表示首字母为i,尾字母为j的最长字符串长度
转移方程:对于字符串abc,
在dp【i】【a】非0的情况下,dp【i】【c】=max(dp【i】【a】+ 3 ,dp【i】【c】)
代码如下:
ll n;
int dp[30][30];
int num(char c){
return c-'a'+1;
}
void solve(){
cin >> n;
for(int ii=1;ii<=n;ii++){
string s; cin >> s;
for(int i=1;i<=26;i++){
if(!dp[i][num(s[0])]) continue;
dp[i][num(s.back())]=max(dp[i][num(s[0])]+(int)s.size(),dp[i][num(s.back())]);
}
dp[num(s[0])][num(s.back())]=max((int)s.size(),dp[num(s[0])][num(s.back())]);
//这句放循环前面可能会导致长度多加一次
}
ll ans=0;
for(int i=1;i<=26;i++) ans=max(1ll*dp[i][i],ans);
cout << ans;
}
背包(线性)dp第一题:撕缎带 cf传送门
思路:dp【i】表示考虑到 i 长度最多段数
开始写了个从n到1的dfs,不出意外的t了,其实是线性dp,赋初值后,后续的值只能从非0的状态转移
代码如下:
ll n;
int a,b,c;
ll dp[4040];
void solve(){
cin >> n >> a >> b >> c;
dp[a]=dp[b]=dp[c]=1;
for(int i=min({a,b,c});i<=n;i++){
if(i>a && dp[i-a]) dp[i]=max(dp[i-a]+1,dp[i]);
if(i>b && dp[i-b]) dp[i]=max(dp[i-b]+1,dp[i]);
if(i>c && dp[i-c]) dp[i]=max(dp[i-c]+1,dp[i]);
}
cout << dp[n];
}
区间dp第一题:祖玛 cf传送门
思路:dp【i】【j】表示消除 i 到 j 的最少时间
先枚举区间长度,再枚举左端点,再枚举左右分界点,这个是常规操作
还有记得特判消除后剩下的珠子会再聚集的情况
代码如下:
ll n;
int c[550];
ll dp[550][550];
void solve(){
cin >> n;
memset(dp,0x3f,sizeof dp);
for(int i=1;i<=n;i++){
cin >> c[i];
dp[i][i]=1;
if(c[i]==c[i-1]) dp[i-1][i]=1;
}
for(int len=2;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
if(c[l]==c[r]) dp[l][r]=min(dp[l+1][r-1],dp[l][r]);
if(r+len-1<=n && c[l]==c[r+len-1])
dp[l][r+len-1]=min(dp[l+1][r-1]+dp[r][r+len-2],dp[l][r+len-1]);
if(l-len+1>=1 && c[l-len+1]==c[r])
dp[l-len+1][r]=min(dp[l-len+2][l]+dp[l+1][r-1],dp[l-len+1][r]);
for(int k=l;k<r;k++)
dp[l][r]=min(dp[l][k]+dp[k+1][r],dp[l][r]);
}
}
cout << dp[1][n];
}
补牛客寒假集训营2一题: 牛客传送门
思路:暴搜
代码如下:
ll n;
string s;
ll y,ans;
set<char>st;
void dfs(string s,int pos){
if(pos==3){
ll num=0;
for(int i=2;i>=0;i--)
num=num*10+s[i]-'0';
if(num%8) return ;
}
if(pos==n){
if(s.back()=='0' && n>1) return ;
ll num=0;
for(int i=n-1;i>=0;i--)
num=num*10+s[i]-'0';
if(num>y || num%8) return ;
ans++,ans%=mod; return ;
}
if(s[pos]>='0' && s[pos]<='9'){
dfs(s,pos+1);
}else if(s[pos]=='_'){
for(s[pos]='0';s[pos]<='9';s[pos]++)
dfs(s,pos+1);
s[pos]='_';
}else{
for(int i='0';i<='9';i++){
if(st.count(i)) continue;
st.insert(i);
auto t=s;
for(auto &j:t)
if(j==s[pos]) j=i;
dfs(t,pos+1);
st.erase(i);
}
}
}
void solve(){
cin >> n >> s >> y;
reverse(s.begin(),s.end());
dfs(s,0);
cout << ans << "\n";
ans=0;
st.clear();
}
星期五:
线性dp第二题:消数拿分 cf传送门
思路:看到数据范围,明显用桶装,对于每个数考虑拿不拿,拿了就从dp【i-2】转移,不拿就从dp【i-1】转移
代码如下:
const int N=2e6+10,M=210;
const int mod=1e9+7;
ll n;
int bu[N];
ll dp[N];
void solve(){
cin >> n;
for(int i=1;i<=n;i++){
int x; cin >> x;
bu[x]++;
}
for(int i=1;i<=1e5;i++)
dp[i]=max(dp[i-2]+1ll*bu[i]*i,dp[i-1]);
cout << dp[100000];
}
状压dp第一题:点菜 cf传送门
一度让我怀疑我到底会不会状压dp(答案是确实不会)
思路:dp【mask】【i】,mask表示压缩后的点菜状态,i表示最后点的一道菜
先枚举所有点菜的状态,再对每个状态枚举rule的x和y进行转移,复杂度O(2^n * n^2)
代码如下:
ll n;
int m,k;
ll dp[1<<19][20];
ll a[20],sa[20][20];
ll ans;
void solve(){
cin >> n >> m >> k;
for(int i=1;i<=n;i++) cin >> a[i];
while(k--){
int x,y,c; cin >> x >> y >> c;
sa[x][y]=c;
}
for(int i=0;i<n;i++) dp[1<<i][i+1]=a[i+1];
for(int mask=0;mask<1<<n;mask++){
for(int i=0;i<n;i++){
if(!(mask&1<<i)) continue;
for(int j=0;j<n;j++){
if(mask&1<<j) continue;
int nmask=mask|(1<<j);
dp[nmask][j+1]=max(dp[mask][i+1]+sa[i+1][j+1]+a[j+1],dp[nmask][j+1]);
}
}
}
for(int mask=0;mask<1<<n;mask++)
if(__builtin_popcount(mask)==m)
for(int i=1;i<=n;i++)
ans=max(dp[mask][i],ans);
cout << ans;
}
星期六:
昨晚牛客练习赛的C 牛客传送门
思路:枚举所有包含k的区间,将其所有物品当作一个物品放入背包,这个过程用前缀和优化,
然后再跑个完全背包,能确保对于k的限制条件能满足
很巧妙一道题,可惜赛时没想出来
代码如下:
ll n;
int m,k;
ll w[2020],v[2020];
ll dp[550];
void solve(){
cin >> n >> m >> k;
for(int i=1;i<=n;i++){
cin >> w[i] >> v[i];
w[i]+=w[i-1],v[i]+=v[i-1];
}
vector<PII>ve;
for(int i=1;i<=k;i++){
for(int j=k;j<=n;j++){
ll ww=w[j]-w[i-1],vv=v[j]-v[i-1];
ve.push_back({ww,vv});
}
}
for(auto [w,v]:ve)
for(int j=w;j<=m;j++)
dp[j]=max(dp[j-w]+v,dp[j]);
for(int i=1;i<=m;i++) cout << dp[i] << " ";
}
下午天梯训练赛,无言
晚上cf div1+2,掉了点分
周日:
dp专题线性dp第三题 子数组数量: cf传送门
思路:dp【i】【j】表示考虑到前 i 个数,长度为 j 的子数组个数
转移:dp【i】【j】=dp【i-1】【j】,如果a【i】%j==0,dp【i】【j】+=dp【i-1】【j-1】
对于每一个a,找出其所有因子,复杂度为O(n* )
但开二维数组空间明显不够,于是用滚动数组的思想,开一维就够了
代码如下:
const int N=2e6+10,M=210;
const int mod=1e9+7;
ll n;
ll dp[N];
void solve(){
cin >> n;
dp[0]=1; //谜之初始化
for(int i=1;i<=n;i++){
int a; cin >> a;
vector<int>tmp;
for(int j=1;j<=a/j;j++){
if(a%j) continue;
tmp.push_back(j);
if(j!=a/j) tmp.push_back(a/j);
}
sort(tmp.begin(),tmp.end(),greater<int>()); //因子从大到小排序
for(auto j:tmp)
dp[j]+=dp[j-1],dp[j]%=mod;
}
ll ans=0;
for(int i=1;i<=n;i++) ans+=dp[i],ans%=mod;
cout << ans;
}