关于我想了很久才想出这题咋做这档事 - 3

#0.0 目录


目录中以下题目皆可点击查看原题

#1.0 P1107 [BJWC2008]雷涛的小猫

其实最开始是要找模拟题做...结果却做了这道题...

#1.1 简单思考

最初读题,便很容易的能看出这是一道dp(话说为什么带着模拟的标签)。

我们将这一棵棵树拼在一起,可以看成是一个方格图,题目就又变成了类似方格取数的样子,状态很好设计,\(f_{i,j}\) 表示在走到格子 \((i,j)\) 时能得到的最大柿子数,那么根据题意,可以迅速地写出转移方程:

\[\begin{align*} f_{i,j} = \max\limits_{1\leqslant k\leqslant n}\{f_{i-d,k}\} + m_{i,j} \end{align*} \]

不过要注意一点,当 \(k=j\) 时,\(d = 1\)

#1.2 记忆化搜索

虽然,我们已经得到了转移方程式,但是,我们仍旧先从最直接的想法出发:搜索 + 记忆化。很好实现,利用 vis[i][j] 判断该格子是否搜索过。

/*P1107 DFS + 记忆化 50pts*/
inline int dfs(int x,int y){
    if (vis[x][y]) //正常的记忆化
      return f[x][y];
    vis[x][y] = true;
    if (x > h) //注意边界
      return m[x][y];
    f[x][y] = m[x][y];
    for (int i = 1;i <= n;i ++){
        if (i == y) //注意特殊情况
          f[x][y] = max(f[x][y],m[x][y] + dfs(x + 1,i));
        else
          f[x][y] = max(f[x][y],m[x][y] + dfs(x + d,i));
    }
    return f[x][y];
}

int main(){
    scanf("%d%d%d",&n,&h,&d);
    for (int i = 1;i <= n;i ++){
        scanf("%d",&num[i]);
        for (int j = 1;j <= num[i];j ++){
            int x;
            scanf("%d",&x);
            m[h - x + 1][i] ++; //注意,树的高度是从下向上递增,但我这里(脑抽)转换成的图行号自上而下递增
        }
    }
    int ans = -INF;
    for (int i = 1;i <= n;i ++) //n个点都有可能开始,不能一次解决
      ans = max(ans,dfs(1,i));
    printf("%d\n",ans);
    return 0;
}

交上去,不出所料,只有一半的分

#1.3 转为简单递推

有了上面的记忆化,转写为递推就不难了,这里要注意边界的处理

/*P1107 N^3 dp 50pts*/
for (int i = h;i >= 1;i --)
  for (int j = 1;j <= n;j ++)
    f[i][j] = m[i][j] + f[i + 1][j];
for (int i = h;i >= d + 1;i --)
  for (int j = 1;j <= n;j ++)
    for (int k = 1;k <= n;k ++)
      if (k != j)
        f[i - d][k] = max(f[i - d][k],f[i][j] + m[i - d][k]);
      else 
        f[i - 1][k] = max(f[i - 1][k],f[i][k] + m[i - 1][k]);
for (int i = h;i >= 1;i --)
  for (int j = 1;j <= n;j ++)
    f[i][j] = max(f[i][j],m[i][j] + f[i + 1][j]);
int ans = -INF;
for (int i = 1;i <= n;i ++)
  ans = max(ans,f[1][i]);

简单一算,时间复杂度是 \(O(n^3)\),显然是不能接受的,果不其然,又是 50pts

#1.4 优化!优化!

#1.4.1 寻找冗杂循环

看上面的代码,不难发现,在

for (int k = 1;k <= n;k ++)
  if (k != j)
    f[i - d][k] = max(f[i - d][k],f[i][j] + m[i - d][k]);
  else 
    f[i - 1][k] = max(f[i - 1][k],f[i][k] + m[i - 1][k]);

这第三层循环中,\(k\) 每增长 \(1\) ,就又有一个 \(f_{i-d,k}\) 加入了可以选择的范围,但我们最终需要的只有一个,不过是第 \(i-d\) 行的最大的 \(f_{i-d,j}\),那么我们完全可以每一次维护一个 \(g_i\) ,表示第 \(i\) 行最大的 \(f\) 是多少,只需加一行代码,省去了一个循环,血赚!

#1.4.2 主要代码

/*P1107 N^2 dp 100pts*/
for (int i = h;i >= 1;i --){
    for (int j = 1;j <= n;j ++)
      f[i][j] = m[i][j] + f[i + 1][j];
    if (i >= d + 1)
      for (int j = 1;j <= n;j ++)
        f[i][j] = max(f[i][j],g[i + d] + m[i][j]);
    for (int j = 1;j <= n;j ++)
      g[i] = max(g[i],f[i][j]);
}
int ans = -INF;
for (int i = 1;i <= n;i ++)
  ans = max(ans,f[1][i]);

#1.4.3 Q&A

为啥还少了两个二层循环?还少了对于列标的特判?

因为这些是我脑抽将转移方式想的复杂了。

先看对于列标的特判,这里无非是 \(f_{i-1,k}\)\(f_{i-d,k}\) 的区别,而且很显然,假如最终的选择是 \(f_{i-d,k}\) ,就意味着 \(f_{i-d,k} > f_{i-1,k}\),但是,\(f_{i-1,k}\) 是从哪里转移来的呢?在他的转移路线上,一定是有 \(f_{i-d,k}\) 这个选择的,那么就有以下两种情况:

  • \(f_{i-1,k}\) 是由 \(f_{i-d,k}\) 转移而来,那么根据转移方程,一定有 \(f_{i-1,k} \geqslant f_{i-d,k}\), 与假设矛盾
  • \(f_{i-1,k}\) 不是由 \(f_{i-d,k}\) 转移而来,那么 \(f_{i-1,k}\) 一定是由一个 \(f_{i-1-d,l}\) 转移而来,就一定有 \(f_{i-d-1,l} > f_{i-d,k}\),且 \(f_{i-1,k} \geqslant f_{i-1-d,k}\),显然就有 \(f_{i-1,k}>f_{i-d,k}\),与假设矛盾

综上,\(f_{i-d,k}\) 一定不会被选。

那两个二层循环,实际是放到了与主要转移同一个循环里,及时更新,效果是相同的。


#2.0 P1083 [NOIP2012 提高组] 借教室

#2.1 简单思考

读题,我们可以这样理解题意:“有一个有 \(n\) 个数的数列 \(\{r_i\}\),有 \(m\) 个操作,每个操作将 \(r_{s_j}\sim r_{t_j}\) 这个区间里的数减去 \(d_i\),找到第一个使数列中出现负数的操作。”打眼一看,区间操作维护区间最小值,用什么?线段树!!!但是线段树码量太大了,所以这里选择另一种方式 ——差分

#2.1.1 神器——差分

首先——差分是什么?

回答这个问题,我们先来说一个熟悉的家伙——前缀和(这里只讲一维的)。

前缀和大家都知道,就是数列前 \(i\) 项的和,有这样的递推式:

sum[i] = sum[i - 1] + a[i];

差分则是前 \(i\) 项相邻两项的差,那给定差分数组去递推求原数组便是这样的:

a[i] = a[i - 1] + diff[i];

由此,我们便能得到以下两点:

  • \(diff_i\) 加上一个数 \(x\) ,相当于给 \(a_k(k\geqslant i)\) 都加上了 \(x.\)

  • 再在 \(diff_{j+1}\) 处减去一个数 \(x\),相当于给 \(a_k(i \leqslant k \leqslant j)\) 都加上了 \(x\).

具体例子这里不再多讲,各位可以自行尝试。

#2.1.1 差分的使用

以上两点可以很方便的用在这里:

对于每一个操作,对原数列的差分数组做如下操作:

diff[s[i]] += d[i];
diff[t[i] + 1] -= d[i];

就相当于改变了该数列的数值。

#2.2 二分

我们看,根据上面的思路,是不是枚举每一个操作,找到第一个使数列出现负数的操作。

那么,对于操作 \(p\) ,有以下两种情况:

  • 操作 \(1 \sim p\) 不会使该数列产生负数,那么显然操作 \(1\sim p-1\) 一定也不会,不需考虑了
  • 操作 \(1\sim p\) 中有一个操作会使数列产生负数,那么操作 \(p + 1\sim m\) 不需考虑了,因为第一个使数列出现负数的操作已经出现

可以看到,这满足一个性质——局部舍弃性,故我们可以使用二分来优化时间复杂度。

#2.3 码代码

/*P1083 二分 100pts*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define INF 0x3fffffff
#define N 2000110
#define ll long long
#define mset(l,x) memset(l,x,sizeof(l));
#define mp(a,b) make_pair(a,b)
using namespace std;

int m,n;
int d[N],s[N],t[N];
int f[N],r[N],need[N];

inline bool check(int x){
    mset(f,0);
    for (int i = 1;i <= x;i ++){
        f[s[i]] += d[i];
        f[t[i] + 1] -= d[i];
    }
    for (int i = 1;i <= n;i ++){
        need[i] = need[i - 1] + f[i];
        if (need[i] > r[i])
          return false;
    }
    return true;
}

int main(){
    scanf("%d%d",&n,&m);
    for (int i = 1;i <= n;i ++)
      scanf("%d",&r[i]);
    for (int i = 1;i <= m;i ++)
      scanf("%d%d%d",&d[i],&s[i],&t[i]);
    int l = 1,r = m,ans = 0;
    if (check(m)){
    	printf("0");
    	return 0;
	}
    while (l <= r){
        int mid = (l + r) >> 1;
        if (check(mid)){
            ans = mid;
            l = mid + 1;
        }
        else
          r = mid - 1;
    }
    printf("-1\n%d",l);
    return 0;
}

#2.4 另一种实现——线段树

线段树来实现这题十分直接,直接暴力维护区间最小值即可,不再多讲,见代码:

/*P1083 线段树 100pts*/
#include <iostream>
#include <cstdio>
#include <vector>
#include <queue>
#include <algorithm>
#define N 1001000
#define INF 0x3fffffff
#define ll long long
#define mset(l,x) memset(l,x,sizeof(l))
using namespace std;

int cnt,n,m;
ll a[N],minn[N * 4],add[N * 4];

inline void pushup(int k){
	minn[k] = min(minn[k << 1],minn[k << 1 | 1]);
}

inline void build(int k,int l,int r){
	if (l == r){
		minn[k] = a[l];
		return;
	}
	int mid = l + r >> 1;
	build(k <<  1,l,mid);
	build(k << 1 | 1,mid + 1,r);
	pushup(k);
}

inline void Add(int k,int l,int r,ll v){
	add[k] += v;
	minn[k] += v;
}

inline void pushdown(int k,int l,int r,int mid){
	if (!add[k])
	  return;
	Add(k << 1,l,mid,add[k]);
	Add(k << 1 | 1,mid + 1,r,add[k]);
	add[k] = 0;
}

inline ll query(int k,int l,int r,int x,int y){
    cnt = max(cnt,k);
	if (l >= x && r <= y)
	  return minn[k];
	int mid = l + r >> 1;
	ll res = INF;
	pushdown(k,l,r,mid);
	if (x <= mid)
	  res = min(res,query(k << 1,l,mid,x,y));
	if (y > mid)
	  res = min(res,query(k << 1 | 1,mid + 1,r,x,y));
	return res;
}

inline void modify(int k,int l,int r,int x,int y,ll v){
	if (l >= x && r <= y){
		Add(k,l,r,v);
		return;
	}
	int mid = l + r >> 1;
	pushdown(k,l,r,mid);
	if (x <= mid)
	  modify(k << 1,l,mid,x,y,v);
	if (y > mid)
	  modify(k << 1 | 1,mid + 1,r,x,y,v);
	pushup(k);
}

int main(){
	scanf("%d%d",&n,&m);
	for (int i = 1;i <= n;i ++)
	  scanf("%lld",&a[i]);
	build(1,1,n);
	for (int i = 1;i <= m;i ++){
		int s,t,x;
		scanf("%d%d%d",&x,&s,&t);
		modify(1,1,n,s,t,-x);
		if (query(1,1,n,s,t) < 0){
		    printf("-1\n%d",i);
		    return 0;
        }
	}
	printf("0");
	return 0;
}


#3.0 P3128 [USACO15DEC]Max Flow P

本题可以使用树上差分解决,而且并不难,但是才疏学浅的我(因为不会)使用了轻重链剖分实现(调试到死),不想多讲QwQ

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
#define INF 0x3fffffff
#define ll long long
#define mset(l,x) memset(l,x,sizeof(l))
#define mp(a,b) make_pair(a,b)
using namespace std;

const int N = 5e4+1e4;

struct Edge{ //建树所需结构体
    int u;
    int v;
    int next;
};
Edge e[N << 1]; 

struct Node{ //线段树所需结构体
    int l,r;
    int ls,rs;
    int maxn;
    int lazy;
};
Node p[N << 2];

int n,m,cnt;
int head[N],top[N],id[N],rk[N];
int d[N],size[N],son[N],f[N];

inline void add_e(int u,int v){ //加边
    e[cnt].u = u;
    e[cnt].v = v;
    e[cnt].next = head[u];
    head[u] = cnt ++;
}

inline void dfs1(int x,int fa,int depth){ //树剖第一个dfs,确定重儿子与父节点
    f[x] = fa;
    d[x] = depth;
    size[x] = 1;
    for (int i = head[x];i != -1;i = e[i].next){
        int v = e[i].v;
        if (v == fa)
          continue;
        dfs1(v,x,depth + 1);
        size[x] += size[v];
        if (size[v] > size[son[x]])
          son[x] = v;
    }
}

inline void dfs2(int x,int t){ //树剖第二个dfs,确定重链链首
    top[x] = t;
    id[x] = cnt;
    rk[cnt ++] = x;
    if (!son[x])
      return;
    dfs2(son[x],t);
    for (int i = head[x];i != -1;i = e[i].next){
        int v= e[i].v;
        if (v != f[x] && v != son[x])
          dfs2(v,v);
    }
}

inline int len(int k){ //
    return p[k].r - p[k].l + 1;
}

inline void pushup(int k){
    p[k].maxn = max(p[p[k].ls].maxn,p[p[k].rs].maxn);
}

inline void add(int k,int x){
    p[k].maxn += x;
    p[k].lazy += x;
}

inline void pushdown(int k){ //基本的下放懒标记
    if (p[k].lazy){
        int ls = p[k].ls,rs = p[k].rs;
        add(ls,p[k].lazy);
        add(rs,p[k].lazy);
        p[k].lazy = 0;
    }
}

inline void build(int k,int l,int r){ //基本的建树
    if (l == r){
        p[k].maxn = 0;
        p[k].l = p[k].r = l;
        return;
    }
    int mid = (l + r) >> 1;
    p[k].ls = cnt ++;
    p[k].rs = cnt ++;
    build(p[k].ls,l,mid);
    build(p[k].rs,mid + 1,r);
    p[k].l = p[p[k].ls].l;
    p[k].r = p[p[k].rs].r;
    pushup(k);
}

inline void modify(int k,int l,int r,int x){ //基本的区间修改
    if (l <= p[k].l && p[k].r <= r){
        add(k,x);
        return;
    }
    pushdown(k);
    int mid = (p[k].l + p[k].r) >> 1;
    if (mid >= l)
      modify(p[k].ls,l,r,x);
    if (mid < r)
      modify(p[k].rs,l,r,x);
    pushup(k);
}

inline int query(int k, int l,int r){ //基本的区间查询
    if (l <= p[k].l && p[k].r <= r)
      return p[k].maxn;
    pushdown(k);
    int mid = (p[k].l + p[k].r) >> 1;
    int ans = -INF;
    if (mid >= l)
      ans = max(ans,query(p[k].ls,l,r));
    if (mid < r)
      ans = max(ans,query(p[k].rs,l,r));
    return ans;
}

inline void update(int x,int y){ //树剖求LCA,并沿途修改点值
    while (top[x] != top[y]){
        if (d[top[x]] < d[top[y]])
          swap(x,y);
        modify(0,id[top[x]],id[x],1);
        x = f[top[x]];
    }
    if (id[x] > id[y])
      swap(x,y);
    modify(0,id[x],id[y],1);
}

int main(){
    mset(head,-1);
    scanf("%d%d",&n,&m);
    for (int i = 1;i < n;i ++){
        int u,v;
        scanf("%d%d",&u,&v);
        add_e(u,v);
        add_e(v,u);
    }
    cnt = 1;
    dfs1(1,0,1);
    dfs2(1,1);
    cnt = 0;
    build(cnt ++,1,n);
    while (m --){
        int a,b;
        scanf("%d%d",&a,&b);
        update(a,b);
    }
    printf("%d",query(0,1,n));
    return 0;
}

#4.0 P1126 机器人搬重物

打眼一看,搜索题。这题思维含量并不多,但情况是真不少,不过根据题意还是不难做出的。

要注意以下两点:

  • 给的地图是描述格子的,我们走的是左上角的格点(但我怎么数都是右下角的格点)
  • 机器人是有宽度的球,所以不能走边界
#include <iostream>
#include <cstdio>
#include <queue>
#include <algorithm>
#include <cstring>
#define N 210
#define INF 0x3fffffff
#define mset(l,x) memset(l,x,sizeof(l))
#define mp(a,b) make_pair(a,b)
using namespace std;

struct Robot{
    int d;
    int x,y;
    int t;
};

int n,m;
int M[N][N],s[2],e[2];
bool vis[N][N][4];
int dir[3][4][2] = {
{{-1,0},{0,-1},{1,0},{0,1}},
{{-2,0},{0,-2},{2,0},{0,2}},
{{-3,0},{0,-3},{3,0},{0,3}}};

queue <Robot> q;

int main(){
    scanf("%d%d",&n,&m);
    Robot b;
    for (int i = 1,j;i <= n;i ++)
      for (j = 1;j <= m;j ++){
          scanf("%d",&M[i][j]);
          if (M[i][j])
            M[i - 1][j - 1] = M[i - 1][j] = M[i][j - 1] = true; 
      }
        
    scanf("%d%d%d%d",&b.x,&b.y,&e[0],&e[1]);
    
    char c;
    cin >> c;
    if (c == 'N')
      b.d = 0;
    else if (c == 'W')
      b.d = 1;
    else if (c == 'S')
      b.d = 2;
    else b.d = 3;
    
    b.t = 0;  //BFS
    q.push(b);
    while (!q.empty()){
        Robot now = q.front();
        q.pop();
        if (now.x == e[0] && now.y == e[1]){
            printf("%d",now.t);
            return 0;
        }
        if (vis[now.x][now.y][now.d])
          continue;
        vis[now.x][now.y][now.d] = true;
        Robot newr;
        for (int i = 0;i < 3;i ++){
            int cx = now.x + dir[i][now.d][0];
            int cy = now.y + dir[i][now.d][1];
            if (cx >= 1 && cx < n && cy >= 1 && cy < m && !M[cx][cy]){
                bool f = false;
                for (int j = 0;j <= i;j ++)
                  if (M[now.x + dir[j][now.d][0]][now.y + dir[j][now.d][1]])
                    f = true;
                if (f)
                  continue;
                newr.d = now.d;
                newr.t = now.t + 1;
                newr.x = cx;
                newr.y = cy;
                q.push(newr);  
            }
        } 
        newr.x = now.x;
        newr.y = now.y;
        newr.t = now.t + 1;
        int dn = (now.d + 1) % 4;
        newr.d = dn;
        q.push(newr);
        dn = now.d - 1;
        if (dn == -1)
          dn = 3;
        newr.d = dn;
        q.push(newr);
    }
    printf("-1");
    return 0;
}

更新日志及说明

更新

  • 初次完成编辑 - \(2021.2.2\)

个人主页

欢迎到以下地址支持作者!
Github戳这里
Bilibili戳这里
Luogu戳这里

posted @ 2021-02-02 17:00  Dfkuaid  阅读(39)  评论(0编辑  收藏  举报