dsa加训

refs: OI Wiki - OI Wiki (oi-wiki.org)

枚举

POJ 2811 熄灯问题

refs : OpenJudge - 2811:熄灯问题

如果要枚举每个灯开或者不开的情况,总计2^30种情况,显然T。

不过我们可以发现:若第i行的某个灯亮了,那么有且仅有第i行和第i+1行的同一列的灯可以影响到它(把它关掉)。

如果某一行的操作已经结束,那么就只有下一行能影响到它了。因此我们可以仅仅枚举第一行的所有操作序列。接下来的每行都会尝试去“填补”上一行产生的问题(也即上一行的没有熄灭的灯)。

在枚举第一行的操作序列时,我们可以发现这是一个长度为6的0-1串,因此转换为十进制后,范围在0-63,我们可以用一个range(64)的列表以及位运算来实现这个操作。

from typing import List

grid = []

m,n = 5,6

for _ in range(m):
    grid.append(list(map(int,input().split())))

# 仅枚举第一行,检查剩余行 操作从全0到全1
f_ops = [x for x in range(64)]

directions = [
    (0,0),(-1,0),(0,1),(1,0),(0,-1)
]

def mat_clone(g:List[List[int]])->List[List[int]]:
    r,c = len(g),len(g[0])
    ans = [[0 for _ in range(c)] for _ in range(r)]
    for i in range(r):
        for j in range(c):
            ans[i][j] = g[i][j]
    return ans

def all_down(g:List[List[int]])->bool:
    return sum([sum(g[i]) for i in range(len(grid))])==0

def is_valid(x:int,y:int)->bool:
    return 0<=x<m and 0<=y<n

def flip(x:int,y:int,puz:List[List[int]]):
    for direction in directions:
        nx,ny = x+direction[0],y+direction[1]
        if is_valid(nx,ny):
            puz[nx][ny] = 1-puz[nx][ny]

def check(fop:int)->bool:
    ops = [[0 for _ in range(n)] for _ in range(m)]
    for i in range(n-1,-1,-1):
        ops[0][i] = fop&1
        fop >>= 1
    puz = mat_clone(grid)
    for i,op in enumerate(ops[0]):
        if op==1:
            flip(0,i,puz)
    
    for i in range(1,m):
        for j in range(n):
            if puz[i-1][j]==1:
                ops[i][j] = 1
                flip(i,j,puz)
    if all_down(puz):
        for row in ops:
            print(' '.join(list(map(str,row))))
        return True
    return False

for f_op in f_ops:
    if check(f_op):
        break

我觉得这道题我写的已经比较优雅了,仅仅60行。

假设行数为m,列数为n,则时间复杂度O(mn*2^n)

递归

LC 437 路径总和Ⅲ

refs: 437. 路径总和 III - 力扣(LeetCode)

向下深搜,维护任意一个子链的路径和到哈希表里,key是路径和,v是出现次数。每次把当前节点加到其下的子链的路径和上去,如果发现为targetSum,则根据v更新答案。

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
        ans = 0

        def check(d:dict,tmp:dict,curr:int):
            nonlocal ans
            for k,v in d.items():
                if k+curr == targetSum:
                    ans += v
                if k+curr in tmp:
                    tmp[k+curr] += v
                else:
                    tmp[k+curr] = v

        def dfs(node:TreeNode)->dict:
            if node is None:
                return {}
            
            nonlocal ans
            ld = dfs(node.left)
            rd = dfs(node.right)

            if node.val == targetSum:
                ans += 1
            res = {}
            res[node.val] = 1
            check(ld,res,node.val)
            check(rd,res,node.val)
            return res
        
        dfs(root)
        return ans

还有种写法更加简洁,运行时间也更短的:维护任一子链前缀和并置入哈希表,key是前缀和,v是出现次数。查询去掉多少个前缀和可以将当前节点和剩余前缀和加起来等于targetSum即可。

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
        m = defaultdict(int)
        m[0] = 1
        

        def dfs(node:TreeNode,prefix:int)->int:
            if node is None:
                return 0
            cnt = 0

            prefix += node.val
            if prefix - targetSum in m:
                cnt += m[prefix-targetSum]
            m[prefix] += 1

            cnt += dfs(node.left,prefix)
            cnt += dfs(node.right,prefix)

            m[prefix]-=1

            return cnt

        return dfs(root,0)

注意维护哈希表时记得回溯时去掉当前prefix,不然可能导致a链上的前缀和被b链上的节点所用。也即

m[prefix] -= 1

时间复杂度:瓶颈哈希表,O(n)

贪心

NOIP2012提高组D1T2

refs: 国王游戏 - Vijos

这题挺难的,思维上想不到,不是dsa知识储备的问题。

设第i个大臣手上的数为

(a_i,b_i)

对于任意合法的(i,i+1),设第i个大臣前的左手数字的乘积为s。金币最大数有以下两种情况:

  1. (i,i+1):

max(\frac{s}{b_i},\frac{s\cdot a_{i}}{b_{i+1}})

  1. (i+1,i):

max(\frac{s}{b_{i+1}},\frac{s\cdot a_{i+1}}{b_i})

若前者站位方式更优于后者,则:

max(\frac{s}{b_i},\frac{s\cdot a_{i}}{b_{i+1}}) < max(\frac{s}{b_{i+1}},\frac{s\cdot a_{i+1}}{b_i})

等价于(通分+约分):

max(b_{i+1},ai\cdot bi) < max(b_{i},a_{i+1}\cdot b_{i+1})

也就是说,对于任意相邻的两个大臣,计算上式,如果前者更大,那么这两个大臣应该互换位置。

注意,这个过程是递归的,如果(i,i+1)换了位置,那么还得检查(i-1,i+1)要不要换位置……直到不需要换位置为止。核心思想是每次交换都把相邻两人之间的最大金币数削减到更小的值

那么这个一直交换的过程相当于什么呢?显然,是冒泡排序

所以在具体实现时,我们可以把每个大臣手里的数封装为一个类实例,然后覆写比较类实例的函数,直接对实例列表排序即可(冒泡排序本身是排序的一种实现方式,既然我们要对整个列表排序,为什么不用更快的排序方式呢?比如py提供的内置快排)

n = int(input())

a,b = tuple(map(int,input().split()))

from functools import total_ordering

@total_ordering
class nh:
    def __init__(self,tup:tuple) -> None:
        self.l = tup[0]
        self.r = tup[1]

    def  __lt__(self,x)->bool:
        if isinstance(x,nh):
            return max(x.r,self.l*self.r) < max(self.r,x.l*x.r)

lrh = [] 
for _ in range(n):
    lrh.append(
        nh(tuple(map(int,input().split())))
    )

lrh = sorted(lrh)

mx = 0
base = a

for nho in lrh:
   mx = max(mx,base//nho.r)
   base *= nho.l

print(mx) 

时间复杂度O(nlogn),瓶颈在排序。

不过这里还有一个问题,我们不妨从归并排序的角度来考虑。

假设我们现在有两个分开的子序列,不妨叫L和R好了,那么在归并时,我们不断从L和R的头部元素中选个最小的放进去。例如,一个归并的结果可能是:

[\;L[0]\;R[0]\;R[1]\;]

也就是说:

L[0]<R[0]<R[1]

这个不等式拆成两份看都没什么问题,毕竟子序列有序,但问题是,为什么

\begin{cases} L[0]<R[0]\\R[0]<R[1] \end{cases}

能推出来

L[0]<R[1]

从直观感受上来说,当比较类实例时,我们比较的是相邻元素在上式中的值,当两个元素不相邻,为什么会有传递律

这个还需要从数学上给出一个数值的证明。我们假设有三个大臣,其手上的数为:

\begin{cases} (l1,r1)\\ (l2,r2)\\ (l3,r3) \end{cases}

根据上述定义,有:

\begin{cases} max(r_2,l_1\cdot r_1) < max(r_1,l_2\cdot r_2)\\ max(r_3,l_2\cdot r_2) < max(r_1,l_3\cdot r_3) \end{cases}

我们知道,若:

\begin{cases} a < b \\ c < d \end{cases}

max(a,c) < max(b,d)

于是有:

max(r_2,r_3,l_1\cdot r_1,l_2\cdot r_2) < max(r_1,r_2,l_2\cdot r_2,l_3 \cdot r_3)

去掉重复项:

max(r_3,l_1\cdot r_1) < max(r_1, l_3 \cdot r_3)

也即(l1,r1)<(l3,r3)的定义。

因此传递律得证,我们可以放心大胆地排序了。

有感

从大二上第一次接触到贪心算法开始,我就觉得这是一个很容易盛产“江湖骗子”的知识点。它不像dp,人家是有严格的状态定义和转移方程的,所以比较令人放心。但是贪心更偏思维和直观感受,或者说就是口胡。说难听点,我觉得除了个别显然且直观的贪心算法的证明(如最短路反证法),其他证明,就算是写在了教科书上,也是“以其昏昏,使人昭昭”,很多时候看到一个证明,我的感受就是:“啊?这就证出来了?”包括我自己写的有关贪心算法的证明,十个里面估计有九个都是在胡扯。这个东西对于我来说几乎无法证明,甚至无法理解别人的证明。

拿这个国王游戏举例,覆写lt然后直接对实例列表进行排序,可以A,而且跑的还很快。可关键在于,如果按照最朴素的邻项交换,那么就会是一个O(n^2)的冒泡排序,虽然跑得慢,但是人家实打实地比较了任意两个元素之间的大小。但如果是归并或者快排,无疑建立在了这个类的实例遵循传递律的基础上,那么传递律又是谁保证的呢?因此我觉得讨论传递律是非常必要的。我在题解区看了一圈,有不少跟我一样直接对实例列表调内置库的sorted的题解,但没人说过传递律的问题。可以想见一些贪心算法的证明中到底遗漏掉了多少细节和重要的地方。

P2949 [USACO09OPEN] Work Scheduling G

refs: P2949 [USACO09OPEN] Work Scheduling G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

比较经典的反悔贪心。先对任务按照截止时间排序。然后逐个遍历,用小根堆维护当前已经做了的任务,如果小根堆长度等同于当前任务的截止时间,说明安排满了,替换profit最小的元素即可。

import heapq

n = int(input())

works = []
q = []
for _ in range(n):
    w = tuple(map(int,input().split()))
    works.append(w)

works = sorted(works,key=lambda tup:(tup[0],tup[1]))

ans = 0
for i,(d,p) in enumerate(works):
    if len(q)<d:
        ans += p
    else:
        pre_p = heapq.heappop(q)
        ans += p - pre_p
    heapq.heappush(q,p)

print(ans)

时间复杂度:O(nlogn),瓶颈排序。

P1209 [USACO1.3] 修理牛棚 Barn Repair

refs: [USACO1.3] 修理牛棚 Barn Repair - 洛谷

被这题恶心了一小下,牛的编号不一定按顺序输入,我服了,这个地方还要恶心我一下是吧。

这题相当于砍m-1刀,砍到两个牛之间距离top m-1大的地方,对差分数组排序即可。

m,s,c = tuple(map(int,input().split()))
indexes = []

for _ in range(c):
    indexes.append(int(input()))
indexes = sorted(indexes)

diff = []
for i in range(1,len(indexes)):
    diff.append(indexes[i]-indexes[i-1])
diff = sorted(diff,reverse=True)

if m>=c:
    print(c)
else:
    rest = max(indexes)-min(indexes)+1
    for i in range(m-1):
        rest -= diff[i]-1

    print(rest)

P2123 皇后游戏

refs: P2123 皇后游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) Write-Ups:皇后游戏——一道连洛谷题解都有错的题目_所有max(ai aj,bi bj,ci cj)的总和-CSDN博客

洛谷把这题数据增强了,不考虑传递律的话A不掉的。以上题解中包含了正确的分析和解答。

from functools import total_ordering

n = int(input())

@total_ordering
class nh:

    def __init__(self,l:int,r:int,d:int) -> None:
        self.l = l
        self.r = r
        self.d = d
    
    def __lt__(self,x)->bool:
        if isinstance(x,nh):
            if self.d!=x.d:
                return self.d<x.d
            if self.d<=0:
                return self.l<x.l
            return self.r>x.r

def solve():
    m = int(input())
    nhs = []
    for _ in range(m):
        l,r = tuple(map(int,input().split()))
        nhs.append(nh(l,r,
                      1 if l>r else (
                        -1 if l<r else 0
                      )))
    nhs = sorted(nhs)

    prec = nhs[0].l+nhs[0].r
    pres = nhs[0].l

    for i in range(1,m):
        pres += nhs[i].l
        prec = max(prec,pres)+nhs[i].r
    print(prec)

while n:
    solve()
    n-=1

差分&&前缀和

P1387 最大正方形

refs: P1387 最大正方形 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

二维前缀和板子。维护二维前缀和,遍历边长k,计算range_sum,比较k*k和range_sum是否相等,维护最大边长即可。

m,n = tuple(map(int,input().split()))

mat = []
for _ in range(m):
    mat.append(list(map(int,input().split())))

prefix = [[0 for _ in range(n+1)]  for _ in range(m+1)]
for i in range(m):
    for j in range(n):
        prefix[i+1][j+1] = prefix[i][j+1] + prefix[i+1][j] - prefix[i][j] + mat[i][j]

def sub_mat_sum(x1:int,y1:int,x2:int,y2:int)->int:
    return prefix[x2][y2] - prefix[x1-1][y2] - prefix[x2][y1-1] + prefix[x1-1][y1-1]

ans = 0
for i in range(1,m+1):
    for j in range(1,n+1):
        for k in range(1,min(m-i+1,n-j+1)+1):
            if k*k == sub_mat_sum(i,j,i+k-1,j+k-1):
                ans = max(ans,k)

print(ans)

P3128 [USACO15DEC] Max Flow P

refs:P3128 [USACO15DEC] Max Flow P - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 树上差分思想:前缀和 & 差分 - OI Wiki (oi-wiki.org)

这里有个非常形象的图,对差分有点基本理解就能看懂。

这道题就是差分数组维护链上点权,记得lca重复增量后(左支右支算两次)要减掉重复的,然后不要影响到lca父节点。然后回溯一遍对差分数组求和就能得到最终答案了。

回溯时加上任意叉的增量以及当前节点的增量,然后返回总和即可。利用了差分维护两端点不会影响链外权值的性质。

但不知道为啥我交上去RE啊?54分,人傻了:

N,K = tuple(map(int,input().split()))

g = [[] for _ in range(N+1)]
for _ in range(N-1):
    x,y = tuple(map(int,input().split()))
    g[x].append(y)
    g[y].append(x)

import math
pows = int(math.ceil(math.log2(N)))
dep = [0 for _ in range(N+1)]
fa = [[0 for _ in range(pows+1)] for _ in range(N+1)]

def dfs(x:int,father:int):
    dep[x] = dep[father]+1
    fa[x][0] = father

    for i in range(1,pows+1):
        fa[x][i] = fa[fa[x][i-1]][i-1]
    
    for c in g[x]:
        if c!=father:
            dfs(c,x)

def lca(s:int,t:int)->int:
    if dep[s]<dep[t]:
        s,t = t,s
    
    for i in range(pows,-1,-1):
        if dep[fa[s][i]]>=dep[t]:
            s = fa[s][i]
    
    if s==t:
        return s

    for i in range(pows,-1,-1):
        if fa[s][i]!=fa[t][i]:
            s,t = fa[s][i],fa[t][i]
    
    return fa[s][0]

dfs(1,0)
diff = [0 for _ in range(N+1)]
for _ in range(K):
    s,t = tuple(map(int,input().split()))
    l = lca(s,t)
    diff[s] += 1
    diff[l] -= 1
    diff[t] += 1
    diff[fa[l][0]] -= 1

pressure = 0
def traverse(x:int,father:int)->int:
    global pressure
    res = 0
    for c in g[x]:
        if c!=father:
            res += traverse(c,x)
    res += diff[x]
    pressure = max(pressure,res)
    return res
traverse(1,0)
print(pressure)

upd-2024-08-01:经LC一位佬的指点,发现是Py递归有层数限制爆栈了,改成手动递归就没事了。

一道树上差分的题目,LCA+差分RE,求助 - 力扣(LeetCode)

N,K = tuple(map(int,input().split()))

g = [[] for _ in range(N+1)]
for _ in range(N-1):
    x,y = tuple(map(int,input().split()))
    g[x].append(y)
    g[y].append(x)

import math
pows = int(math.ceil(math.log2(N)))
dep = [0 for _ in range(N+1)]
fa = [[0 for _ in range(pows+1)] for _ in range(N+1)]

def dfs():
    stk = [(1,0)]
    while stk:
        x,father = stk.pop()
        
        dep[x] = dep[father]+1
        fa[x][0] = father

        for i in range(1,pows+1):
            fa[x][i] = fa[fa[x][i-1]][i-1]
        
        for c in g[x]:
            if c!=father:
                stk.append((c,x))

def lca(s:int,t:int)->int:
    if dep[s]<dep[t]:
        s,t = t,s
    
    for i in range(pows,-1,-1):
        if dep[fa[s][i]]>=dep[t]:
            s = fa[s][i]
    
    if s==t:
        return s

    for i in range(pows,-1,-1):
        if fa[s][i]!=fa[t][i]:
            s,t = fa[s][i],fa[t][i]
    
    return fa[s][0]

dfs()
diff = [0 for _ in range(N+1)]
for _ in range(K):
    s,t = tuple(map(int,input().split()))
    l = lca(s,t)
    diff[s] += 1
    diff[l] -= 1
    diff[t] += 1
    diff[fa[l][0]] -= 1

pressure = 0

def traverse()->int:
    stk,order = [(1,0)],[]
    while stk:
        x,father = stk.pop()
        order.append((x,father))
        for c in g[x]:
            if c!=father:
                stk.append((c,x))
    
    for x,father in reversed(order):
        for c in g[x]:
            if c!=father:
                diff[x]+=diff[c]
    return max(diff)

pressure = traverse()
print(pressure)

改完之后还是因为卡常被T了几个点,但RE是没了。

B3612 【深进1.例1】求区间和

refs:B3612 【深进1.例1】求区间和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

简单前缀和板子,无需多言。

n = int(input())

nums = list(map(int,input().split()))

m = int(input())
prefix = [0]
tmp = 0

for x in nums:
    tmp += x
    prefix.append(tmp)

for _ in range(m):
    a,b = tuple(map(int,input().split()))
    print(prefix[b]-prefix[a-1])

U69096 前缀和的逆

refs:U69096 前缀和的逆 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

python比较被这题克制,因为他卡常。无奈只能c++优化了。题本身很简单。前缀和差分还原元素。

#include <stdio.h>
#include <vector>

using namespace std;

int main(){
    int n;
    scanf("%d",&n);

    int a=0;int b=0;
    for(int i=0;i<n;i++){
        scanf("%d",&b);
        printf("%d ",b-a);
        a=b;
    }
}

JOI2007_A 最大の和

refs:A - 最大の和

这题感觉一次遍历滑窗就行啊,但不知道为啥A不掉。

#include <iostream>
#include <vector>

using namespace std;

int main(){
    int n,k;
    cin>>n>>k;

    vector<int> v,prefix;
    int a;
    for(int i=0;i<n;i++){
        cin >> a;
        v.push_back(a);
    }

    prefix.push_back(0);
    for(int i=0;i<n;i++){
        prefix.push_back(v[i]+prefix.back());
    }

    int mx = prefix[k];
    for(int i=k;i<=n;i++){
        mx = max(mx,prefix[i]-prefix[i-k]);
    }

    printf("%d\\n",mx);
}

P3131 [USACO16JAN] Subsequences Summing to Sevens S

refs:[USACO16JAN] Subsequences Summing to Sevens S - 洛谷

前缀和维护区间和很trivial。怎么做到在O(n)时间内找最长区间比较难。

定义dp(i,j),其中0≤j<7,为从第i个元素开始能组成的和为7的连续子数组的最大长度,则:

$$ dp(i,j) = max(dp(i,j),1+dp(i+1,(j+7-nums[i]\%7)\%7) $$

n = int(input())
nums = [int(input()) for _ in range(n)]

# dp = [[-1 for _ in range(7)] for _ in range(n)]
tr = [[0 for _ in range(7)] for _ in range(n)]
# dp[n-1][nums[n-1]%7] = nums[n-1]
tr[n-1][nums[n-1]%7] = 1

ans = 0
for i in range(n-2,-1,-1):
    # dp[i][nums[i]%7] = nums[i]
    tr[i][nums[i]%7] = 1
    for j,x in enumerate(tr[i+1]):
        rest = (j+nums[i])%7
        if x:
            tr[i][rest] = max(tr[i][rest],1+tr[i+1][j])
        # if x!=-1:
            # dp[i][rest] = max(dp[i][rest],x+nums[i]) 
            
    ans = max(ans,tr[i][0])

print(ans)

当然这题的蠢办法就是前缀和然后倒序遍历区间长度逐个检查。O(n^2)的。我这个时间空间都是O(n)。

上面代码里的dp数组是我一开始搞错了以为要求最大的能被7整除的和,结果是求区间长度,乐(

P6067 [USACO05JAN] Moo Volume S

refs:[USACO05JAN] Moo Volume S - 洛谷

这题好像必须排序吧。计数题,先差分,然后前缀和还原时记得成倍计算。因为每增加一个元素就要增加该元素和前面元素的差分。

N = int(input())

indexes = sorted([int(input()) for _ in range(N)])
ans = 0

diff = [0]

for i in range(1,N):
    diff.append((indexes[i]-indexes[i-1])*i+diff[-1])
    ans += diff[-1]

print(ans*2)

然后这题比较坑的地方在于这个边是双向的。所以答案要乘以2。

最近公共祖先 

LCA两大板子:

  1. 倍增
  2. Tarjan(之前写过)

P3379 【模板】最近公共祖先(LCA)

refs:【模板】最近公共祖先(LCA) - 洛谷 视频讲解:D09 倍增算法 P3379【模板】最近公共祖先(LCA)_哔哩哔哩_bilibili

Lowest Common Ancestor-CSDN博客

洛谷有语言歧视啊,同样的算法py可能会T,我这个只能拿70,C++能拿满:

import math

N,M,S = tuple(map(int,input().split()))

g = [[] for _ in range(N+1)]
pows = int(math.ceil(math.log2(N)))

dep = [0 for _ in range(N+1)]
fa = [[0 for _ in range(pows+1)] for _ in range(N+1)]

for _ in range(N-1):
    x,y = tuple(map(int,input().split()))
    g[x].append(y)
    g[y].append(x)

# 倍增预处理ST表
def dfs(x:int,father:int):
    dep[x] = dep[father]+1
    
    fa[x][0] = father
    for i in range(1,pows+1):
        fa[x][i] = fa[fa[x][i-1]][i-1]
    
    for c in g[x]:
        if c!=father:
            dfs(c,x)

# LCA
def lca(s:int,t:int)->int:
    if dep[s]<dep[t]:
        s,t = t,s
    
    # 先跳到同一层
    for i in range(pows,-1,-1):
        if dep[fa[s][i]] >= dep[t]:
            s = fa[s][i]
    
    if s==t:
        return s
    
    # 再一起跳到lca的下一层
    for i in range(pows,-1,-1):
        if fa[s][i]!=fa[t][i]:
            s,t = fa[s][i],fa[t][i]
    
    return fa[s][0]

dfs(S,0)
for _ in range(M):
    s,t = tuple(map(int,input().split()))
    print(lca(s,t))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值