P1944 最长括号匹配 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
最长括号匹配:
1.对于这道题我们首先确定一下dp方程表达:
dp[i]表示以i结尾的最长连续括号方程:我们还要记录一下此时的end位置为了记录最后答案:
现在思考我们的dp如何转移:
1.首先我们dp[i]此时的i是')'//']' 毕竟是要以此为结尾的 所以 我们要找的是'('//'['
现在问题转移到到哪里找这个'(''//'['了 我们尝试思考这个位置是否和之前的dp有关 毕竟是转移方程
我们可以发现对于dp[i-1]此时他们也是一个完整的括号匹配序列 所以我们必须在这个长度后面经行匹配:就是说 (()()())下标从1开始 此时6 为dp[7]的最长 我们是不是应该往i-1-dp[i-1]位置往前去匹配呢
所以此时我们的dp的转移判断就完成了
s[i]==')'&&s[i-1-dp[i-1]]=='('||s[i]==']'&&s[i-1-dp[i-1]]=='['
那么接下来考虑如何转移:
因为此时当满足条件的时候我们一定要将中间的所有值的加上去吧 还有我们的2
那么此时:
dp[i]=2+dp[i-1];
假如我们直接交上去的话会发现这玩意是错误的 那么我们应该是还有啥情况考虑不周到
我们设想此时匹配完后的序列为(()()()) 那很容易发现此时我们这个序列是可以和前面的序列接轨的就是说其可以再在之前经行添加匹配 所以此时的dp应该为:
dp[i]=2+dp[i-1]+dp[i-dp[i-1]-2];
那可能有人会思考为什么此时我们不用继续往前呢 :
其实我们可以从dp的含义来理解此时的dp代表的是由这点出发可以到达的最长序列 所以此时再往前是不可能出现和其接轨的序列的
可能有一点绕口 就是说 假如前面存在可以继续接轨的序列:(就是 ()前面存在()[]这类序列) dp[i-dp[i-1]-2]已经将这些包含了
#include<bits/stdc++.h>
using namespace std;
#define int long long
string s;
const int maxn=1e6+10;
int dp[maxn];
signed main()
{
cin>>s;
int len=s.length();
int end=0;
s='!'+s;
int maxlen=0;
for(int i=1;i<=len;i++)
{
if(s[i]==')'&&s[i-dp[i-1]-1]=='('||s[i]==']'&&s[i-dp[i-1]-1]=='[')
{
dp[i]=2+dp[i-1]+dp[i-2-dp[i-1]];
if(dp[i]>maxlen)
{
end=i;
maxlen=dp[i];
}
}
}
for(int i=end-maxlen+1;i<=end;i++)
cout<<s[i];
}
P2340 [USACO03FALL]Cow Exhibition G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
我们考虑如何处理这道题的dp
首先先考虑最暴力的dp
dp[i][j][k]代表此时前i个奶牛 j智商和k情商最大的智商和情商和
由于可能存在负数所以此时我们需要维护一个偏移量 让其为正数 但此时我们会发现:
加上偏移量的话此时:j:8e5 k:8e5 所以此时是不大可能的
考虑到我们最后dp结果也胡此时表达式子有关 我们考虑能不能消掉一维 :
就是说此时dp[i][j]=max(i+j)//我们利用滚动数组的思想消掉了前i这个玩意 此时i j为上面的 j k
那么此时我们可以维护一个固定的i 寻找最小的j 就是 :维护一个dp[i]代表此时再i的智商下最大的j
那么此时 我们就成功将其从3维消到1维了
考虑如何转移 :其实就是把智商当成容量 情商代表重量的dp
此时还有一个重点:就是枚举顺序:
对于状态j我们当初学习背包的时候都知道01要倒着枚举当时此时我们 要思考他的具体含义 不然这道题是死活过不去的 :
我们此时dp[j]转移的话是要再以未更新的dp[j-a[i]]来转移的 这一点十分重要:这代表着我们如何枚举转移:
由于此时a[i]为负数:那么我们此时会发现一点:我们更新的话 dp[j]假如逆序来的话:此时dp[j-a[i]]是已经被更新过的点:
所以此时会变为完全背包
所以我们要正序枚举(这也是一个非常好的知识点)
/*
1.学习了如何处理有下界的背包问题
*/
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 400000+10;
long long dp[maxn << 1], a[410], b[410];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i] >> b[i];
}
memset(dp, -0x3f, sizeof dp);
dp[400000] = 0;
for (int i = 1; i <= n; i++)
{
if (a[i] >= 0)
{
for (int j = 800000; j >= a[i]; j--)
{
dp[j] = max(dp[j], dp[j - a[i]] + b[i]);
}
}
else
{
for (int j = 0; j <= 800000+a[i]; j++)
{
dp[j] = max(dp[j], dp[j - a[i]] + b[i]);
}
}
}
long long ans = 0;
for (int i = 400000; i <= 800000; i++)
{
if (dp[i] > 0)
{
ans = max(ans, i + dp[i] - 400000);
}
}
cout << ans;
}
P1122 最大子树和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题与其说是树形dp 就是dfs加贪心的感觉:
我们假设dp[i]代表以i为根的子树剪枝后的最大美丽值考虑如何转移:
我们可以贪心的思考:
假如此时他的一个子节点的dp值是大于0的话 我们不减他的话收益是不是会变得更大:
那么我们就可以发现 只要dp[y]>=0 dp[x]+=dp[y] ;
此时最后得答案一定最优:
#include<iostream>
#include<algorithm>
using namespace std;
#define int long long
const int maxn = 16100;
int dp[maxn];
struct node
{
int to, next;
}e[maxn<<1];
int a[maxn],n;
int head[maxn], cnt;
void add(int u, int v)
{
e[++cnt].next = head[u];
e[cnt].to = v;
head[u] = cnt;
}
void dfs(int u, int fa)
{
dp[u] = a[u];
for (int i = head[u]; i; i = e[i].next)
{
int y = e[i].to;
if (y == fa)continue;
dfs(y, u);
if (dp[y] >= 0)
dp[u] += dp[y];
}
}
signed main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <n; i++)
{
int a1, b; cin >> a1 >> b;
add(a1, b), add(b, a1);
}
dfs(1, 0);
int ans =dp[1];
cout << ans;
}
P2016 战略游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题我们可以比较明确是树形dp
那么我们对于树形dp状态的处理是依据他的条件:
1.我们可以发现此时每个点都有两种状态: 1.取或者不去
所以我们设dp[i][0/1]代表此时以i为根的子树 取根节点 或不取根节点 的情况下要多少个点将子树覆盖:
考虑如何转移:
dp[i][0]+=dp[y][1];//这是因为此时我们的根节点连的那些变是没有被覆盖的 代表此时我们必须选择dp[y][1]将那些边覆盖:
那对于dp[i][1]呢:这个就好考虑两种情况:
由于此时他和儿子连的边均有自己看着 所以他的儿子可来可不来:
所以我们选择其中较小的就好了:
此时dp[i][1]+=min(dp[y][0],dp[y][1]);
#include<iostream>
#include<algorithm>
using namespace std;
#define int long long
const int maxn = 2010;
int head[maxn], cnt;
struct node
{
int to, next;
}e[maxn<<1];
int dp[maxn][2];
void add(int u, int v)
{
e[++cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
}
void dfs(int u,int fa)
{
dp[u][1] = 1;
for (int i = head[u]; i; i = e[i].next)
{
int y = e[i].to;
if (y == fa)
continue;
else
{
dfs(y, u);
dp[u][0] += dp[y][1];
dp[u][1] +=min(dp[y][0],dp[y][1]);
}
}
}
signed main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
int a, b;
cin >> a >> b;
for (int j = 1; j <= b; j++)
{
int x;
cin >> x;
add(a, x);
add(x, a);
}
}
dfs(0, -1);
cout << min(dp[0][0],dp[0][1]);
}
P1273 有线电视网 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题的话也是树形dp
我们考虑如何确定表达式:
我们题目的要求为在 不亏本的情况下尽可能的多人参加:
那么我们的第一反应就是dp[i][j]当此时我们以i为根节点的盈利状况为j的最大人数:(这玩意我想了挺久的 不知道怎么写)
我们可以考虑另一种写法:dp[i][j]代表此时我们以i为根节点 选取j个人的最大盈利状态 (此时我们是将 表达式中的要求的结果调换)(这个思想以后说不定会很好用)
此时考虑如何转移:我们可以参照背包:
枚举此时的总人数 和此时的给该子节点的人数
即dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-e[i].dis);
对于树形背包问题 我们要学会合理剪枝 避免被极限数据卡
同时要转移预处理:
#include<bits/stdc++.h>
using namespace std;
#define int long long
string s;
const int maxn=3e3+10;
int head[maxn], cnt;
struct node
{
int to, next,dis;
}e[maxn<<1];
int dp[maxn][maxn];
int siz[maxn];//代表此时该节点的叶子节点数
void add(int u, int v,int d)
{
e[++cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
e[cnt].dis=d;
}
void dfs(int u,int fa)
{
int tot=0;
for(int i=head[u];i;i=e[i].next)
{
int y=e[i].to;
if(y==fa)continue;
tot++;
dfs(y,u);
siz[u]+=siz[y];//代表前i个树有多少个节点
//cout<<u<<' '<<' '<<y<<' '<<siz[u]<<endl;
for(int j=siz[u];j>=0;j--)
{
for(int k=min(siz[y],j);k>=0;k--)
{
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[y][k]-e[i].dis);
}
}
}
if(tot==0)
{
siz[u]=1;
}
}
signed main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n-m;i++)
{
int k;
cin>>k;
for(int j=1;j<=k;j++)
{
int to,d;
cin>>to>>d;
add(i,to,d);
add(to,i,d);
}
}
//预处理:
memset(dp,-0x3f,sizeof dp);
for(int i=1;i<=n;i++)
dp[i][0]=0;
for(int i=n-m+1;i<=n;i++){
cin>>dp[i][1];
}
dfs(1,0);
for(int i=n;i>=0;i--)
{
if(dp[1][i]>=0)
{
cout<<i;
return 0;
}
}
cout<<0<<endl;
}
P1284 三角形牧场 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
对于这道题 我们可以发现假如我们确定了两条边的话 第三条边的长度也就会确定
假如我们直接思考的话 可能会思考到想出一个dp其结果为此时最小的面积 当结果是 这条路可能行不通 所以我们参考上述的一道题 转换思想将面积这个信息看看能不能变为dp表达式的一部分
我们可以发现 确定面积->确定3条边->确定周长和两条边
此时我们将目标放在了确定两条边的长度上面 思考此时如何dp?
我们可以直接dp[i][j]代表一条边为i,一条边为j是否可能
这样的话dp转移就为:
dp[i][j]=dp[i-len][j]|dp[i][j-len]|dp[i][j];//分别对应着放在第一条边 第二条边和第3条边
我们考虑如何枚举此时边长:顺序还是逆序:由于此时我们一条边只能选择1次且边长为正 所以此时我们要逆序枚举
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=810;
bool dp[maxn][maxn];
int a[41],n;
bool check(int a,int b,int c)
{
if(c>a)
swap(c,a);
if(c>b)
swap(c,b);
if(a+c>b)
return 1;
else
return 0;
}
signed main()
{
cin>>n;
int len=0;
for(int i=1;i<=n;i++)
{
cin>>a[i];
len+=a[i];
}
dp[0][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=800;j>=0;j--)
{
for(int j1=800;j1>=0;j1--)
{
dp[j][j1]|=dp[j][j1];
if(j>=a[i])
dp[j][j1]|=dp[j-a[i]][j1];
if(j1>=a[i])
dp[j][j1]|=dp[j][j1-a[i]];
}
}
}
int ans=0;
for(int i=1;i<=800;i++)
{
for(int j=1;j<=800;j++)
{
if(dp[i][j])
{
int c=len-i-j;
if(check((int)c,i,j)){
double now=len;
now/=2;
ans=max(ans,(int)(100*(sqrt(now*(now-i)*(now-j)*(now-c)))));
}else
continue;
}
}
}
if(ans!=0)
cout<<ans;
else
cout<<-1;
}
P1282 多米诺骨牌 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
我们发现假如要将其转化为dp的话我们最后的结果是要维护一个两个条件 1 abs(差) 2.最小转移次数很明显 这两玩意不能同时表达 也没有具备上面明显的关系 所以我们尝试利用将其中一种转变为表达式 很明显此时差值变为表达式是更加好的决定 那么此时dp[i][j]代表上面的点数是i 下面点数是j需要的最小的转移次数 我们考虑如何转移:
先考虑dp[i][j][k]//j代表此时我们上面的点数 k代表此时我们下面的点数 所以此时我们转移的话:
dp[i][j][k]=dp[i-1][j-a[i]][k-b[i]]//没有转化的变化
dp[i][j][k]=dp[i-1][j-b[i]][k-a[i]]+1;
我们可以将其视为分组二维背包
当很明显我们发现此时数据范围不允许 所以我们思考如何优化 这时候就利用到之前学习到的知识了 我们尝试将其中一位维护掉 因为此时我们知道总数是多少所以我们可以不用理会第二行 我们就维护第1行就好了
dp[i][j]代表此时前i行维护为j最小需要的转移次数
可以利用滚动数组优化 但没有必要:
#include<bits/stdc++.h>
using namespace std;
const int maxn=6e3+10;
int dp[1010][maxn];
int a[maxn],b[maxn];
signed main()
{
int n;
cin>>n;
int sum=0;
for(int i=1;i<=n;i++)
{
cin>>a[i]>>b[i];
sum=sum+a[i]+b[i];
}
memset(dp,0x3f,sizeof dp);
dp[0][0]=0;
for(int i=1;i<=n;i++)
{
for(int j=6000;j>=0;j--)
{
if(j>=a[i])
dp[i][j]=min(dp[i][j],dp[i-1][j-a[i]]);
if(j>=b[i])
dp[i][j]=min(dp[i][j],dp[i-1][j-b[i]]+1);
}
}
int ans=1e9;
int now=1e9;
for(int i=1;i<=6000;i++)
{
if(dp[n][i]<=n)
{
ans=min(ans,abs(sum-i-i));
}
}
//cout<<ans<<endl;
for(int i=1;i<=6000;i++)
{
if(abs(sum-i-i)==ans)
{
now=min(now,dp[n][i]);
}
}
cout<<now;
}
P1941 [NOIP2014 提高组] 飞扬的小鸟 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题我们考虑如何dp
先不考虑范围:因为范围没有明显的区分我们该利用何种dp
由于每个衡坐标我们都会有一个新的上升和下降高度 我们此时维护一下这个
dp[i]代表此时我们衡坐标为i 由于此时能否通过第j个阻碍物和我们此时高度有一定关系
所以我们第二维枚举j代表此时的高度 dp[i][j] 我们考虑能不能变为考虑第j个阻碍物?
由于此时阻碍物的坐标是一定范围 要是直接搞的话可能不太好确定我们是从上一个阻碍物的那个位置转移来的
所以此时 我们dp[i][j]代表通过坐标(i,j)最小的点击次数 而他通过多少的阻碍物的话可以一边维护一边判断
这道题还挺坑的写了我两小时:
对于完全背包的枚举,和如何处理规定上界的问题
这玩意卡了我好久从45-70-95-100
也是学习到了新的有用的知识吧:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=2e3+10;
int dp[maxn*10][maxn];
bool vis[maxn];
struct node
{
int up,down;
}a[maxn*10];
int n,m,k;
struct node1
{
int id,low,hight;
inline bool operator<(const node1&jk)const
{
return id<jk.id;
}
}b[maxn*10];
signed main()
{
cin>>n>>m>>k;
for(int i=0;i<n;i++)
{
cin>>a[i+1].up>>a[i+1].down;
}
for(int i=1;i<=k;i++)
{
cin>>b[i].id>>b[i].low>>b[i].hight;
}
sort(b+1,b+1+k);
memset(dp,0x3f,sizeof dp);
for(int i=1;i<=m;i++)
dp[0][i]=0;
int now=1;
for(int i=1;i<=n;i++)
{
bool flag=0,ok=0;
if(b[now].id==i)
flag=1;
//开始转移
for(int j=a[i].up+1;j<=m+a[i].up;j++)
{
dp[i][j]=min(dp[i-1][j-a[i].up]+1,dp[i][j-a[i].up]+1);
}
for(int j=m+1;j<=m+a[i].up;j++)
dp[i][m]=min(dp[i][m],dp[i][j]);
for(int j=1;j<=m-a[i].down;j++)
{
dp[i][j]=min(dp[i][j],dp[i-1][j+a[i].down]);
}
if(flag)
{
for(int j=b[now].low+1;j<b[now].hight;j++)
{
if(dp[i][j]<0x3f3f3f)
{
ok=1;
break;
}
}
if(ok==0)
{
cout<<0<<endl;
cout<<now-1;
return 0;
}
for(int j=0;j<=b[now].low;j++)
dp[i][j]=0x3f3f3f;
for(int j=b[now].hight;j<=m;j++)
dp[i][j]=0x3f3f3f;
now++;
}
}
cout<<1<<endl;
int ans=1e18;
for(int i=1;i<=m;i++)
{
ans=min(ans,dp[n][i]);
}
cout<<ans;
}
P1040 [NOIP2003 提高组] 加分二叉树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题的话 我们可以发现可以将其看做一个记忆化搜索:就是每次我们考虑此时的根节点应该是谁然后往下遍历
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
#define int long long
const int maxn = 1e2;
int dp[maxn][maxn];
int root[maxn][maxn];
int a[maxn];
int dfs(int l, int r)//代表此时的区间范围
{
if (l > r)
return 1;
if (l == r)
{
dp[l][r] =a[l];
root[l][r] = l;
return dp[l][r];
}
if (dp[l][r])return dp[l][r];
for (int i = l; i <=r; i++)
{
int now = dfs(l,i-1)*dfs(i+1, r) + a[i];
if (now >dp[l][r])
{
dp[l][r] = now;
root[l][r] = i;
}
}
return dp[l][r];
}
void dfs1(int l, int r)
{
if (root[l][r] == 0)return;
cout << root[l][r] << ' ';
if (l == r)return;
dfs1(l, root[l][r] - 1);
dfs1(root[l][r] + 1, r);
}
signed main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)cin >> a[i];
dfs(1, n);
cout << dp[1][n]<<endl;
dfs1(1, n);
}
P4170 [CQOI2007]涂色 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
我们可以从数据范围得知这玩意大概率是区间dp 或者变样的背包之类的题
我们考虑 区间dp 此时我们设dp[l][r]代表着我们此时将区间l,r 涂成题目要求的区间颜色时需要的最小次数 我们考虑此时如何转移:
可以发现 1点 我们最外层的话一定是最早涂的 因为此时我们中间的色要覆盖在他们上面 这给了我们启发 看此时最端点两者颜色是不是相同的 假如相同的话我们是不是可以少了一次涂色
我们继续思考还有没有哪些可以减少操作次数 可以发现假如我们将两个区间进行合并的话此时我们合并的那个部位假如颜色相同的话 是不是也可以减少一次涂色次数
还有我们正常将两者合并的消耗
#include<bits/stdc++.h>
using namespace std;
#define int long long
int dp[60][60];
string s;
signed main()
{
cin>>s;
int len=s.length();
s='~'+s;
memset(dp,0x3f,sizeof dp);
for(int i=1;i<=len;i++)
dp[i][i]=1;//初始化
for(int le=2;le<=len;le++)
{
for(int begin=1;begin<=len-le+1;begin++)
{
int end=begin+le-1;
if(s[begin]==s[end])
{
dp[begin][end]=min(dp[begin+1][end],dp[begin][end-1]);
}
for(int k=begin;k<end;k++)
{
dp[begin][end]=min(dp[begin][end],dp[begin][k]+dp[k+1][end]);
if(s[k]==s[k+1])
{
dp[begin][end]=min(dp[begin][end],dp[begin][k]+dp[k+1][end]-1);
}
}
}
}
cout<<dp[1][len];
}
CF149D Coloring Brackets - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题我也不会 我只是按题解的思路来写的 至于为啥要这样写 我也没头绪:
#include<iostream>
#include<algorithm>
#include<stack>
#include<string>
using namespace std;
#define int long long
string s;
const int maxn = 710;
const int mod = 1e9 + 7;
int dp[maxn][maxn][3][3];
stack<int>st;//用来储存合法配对
int n, match[maxn];
void dfs(int l, int r)
{
if (l + 1 == r)
{
dp[l][r][0][1] = dp[l][r][0][2] = dp[l][r][1][0] = dp[l][r][2][0] = 1;
}
else if (match[l] == r)//代表此时配对
{
dfs(l + 1, r - 1);//继续往下搜索
for (int i = 0; i <= 2; i++)
{
for (int j = 0; j <= 2; j++) {
if (j != 1)
(dp[l][r][0][1] += (dp[l + 1][r - 1][i][j])) %= mod;
if (j != 2)
(dp[l][r][0][2] += (dp[l + 1][r - 1][i][j])) %= mod;
if (i != 1)
{
(dp[l][r][1][0] += (dp[l + 1][r - 1][i][j])) %= mod;
}
if (i != 2)
(dp[l][r][2][0] += (dp[l + 1][r - 1][i][j])) %= mod;
}
}
}
else
{
dfs(l, match[l]), dfs(match[l]+1, r);//划分为两段区间 一段合法 一段继续往下分
for (int i = 0; i <= 2; i++)
{
for (int j = 0; j <= 2; j++)
{
for (int p = 0; p <= 2; p++)
{
for (int q = 0; q <= 2; q++)
{
if (j == 1 && p == 1 || (j == 2 && p == 2))continue;//代表此时配对的两个是相同的
dp[l][r][i][q] = (dp[l][r][i][q] + dp[l][match[l]][i][j] * dp[match[l] + 1][r][p][q] % mod) % mod;
}
}
}
}
}
}
signed main()
{
cin >> s;
int n = s.length();
for (int i = 0; i < n; i++)
{
if (s[i] == '(')st.push(i);
else
{
match[st.top()] = i, match[i] = st.top(), st.pop();
}
}//配对
dfs(0, n - 1);
int ans = 0;
for (int i = 0; i <= 2; i++)
{
for (int j = 0; j <= 2; j++)
{
ans = (ans + dp[0][n - 1][i][j]) % mod;
}
}
cout << ans;
}