算法竞赛

特殊函数

  • 海伦公式
    p = (a+b+c)/ 2
    S = sqrt(p * (p-a) * (p-b) * (p-c))

  • 排列组合
    Cnm = Anm / Amm = n! / m!(n-m)! / n * (n-1)…(n-m+1) / m!
    Anm = n * (n-1)…(n-m+1) / n! / (n-m)!

  • 二叉树
    数组表示树:2i+1,2i+2左右子树,父结点 (i-1)/ 2

Java

  • == 如果在[-128,127]会被cache缓存,比较值,超过这个范围则比较的是对象是否相同

  • Map.getOrDefault(Object key, V defaultValue) 方法的作用是:当Map集合中有这个key时,就使用这个key对应的值;如果没有就使用默认值defaultValue。

  • Arrays.stream(nums).distinct().count() :去重统计不同个数

  • String.getBytes():获取字符串中每一个字符的ASCII的值

  • Integer.toBinaryString(int):将整数转化为二进制字符串

  • String类:使用replace()函数是返回一个替换后的字符串,而字符串本身并未改变。因此需要将返回值赋予自身变量,例如str = str.replace(oldChar,newChar)。

  • StringBuilder型字符串无法直接使用equals()比较两个字符串是否相等,因为StringBuilder类的equals()没有重写,内部依然使用了**==的比较方法,所以即使两个相等的字符串比较也为false**。因此需要将两个StringBuilder类型字符串用toString()转换为String类型后再用equals()进行比较。

  • StringBuilder类的reverse()方法是对原对象进行翻转

  • StringBuilderStringBuffer效率高但线程不安全

  • 在 Java 中,我们通过取小数部分(利用 % 1)来检查数字是否是整数,并检查它是否是 0。

  • String.format("%4d", num) : 右对齐占4位
    System.out.printf("%-10s",“abc”); //输出10列,左对齐(-号表示左对齐)
    System.out.printf("%8d",23); //输出8列, 右对齐
    %10s : 使字符串右对齐输出,不足10位,前加空格。
    System.out.printf("%d\n%d\n%.2f", max, min, (double)sum / n);

  • 求二进制位数
    (int) Math.ceil(Math.log(num) / Math.log(2))

  • PriorityQueue<int[]> minHeap = new PriorityQueue<>(Comparator.comparingInt(a -> a[1])); 优先队列,重写比较器

static Comparator comparingInt​(ToIntFunction<? super T>
keyExtractor)
接受提取的一个函数int从类型分类键T ,并返回一个Comparator ,通过该排序关键字进行比较。
如果指定的功能也可串行化,则返回的比较器是可序列化的。

  • Collections.sort(list,Comparator.comparingInt(Order::getValue));
  • sum(i%2==int(v) for i, v in enumerate(s))(生成交替二进制字符串的最少操作数)

Python

  • dict = collections.Counter(str) :返回一个统计不同字符数量的字典对象
  • dict.values():返回所有值组成的列表
  • dict.items():返回键值对元组组成的列表
  • chr():将ASII码转为字符
  • ord():将字符转为ASCII码
  • ’{0:b}’.format(int(a, 2) + int(b, 2)):将整数转化为二进制数字符串表示,int(str, 2) :将字符串转化为指定进制整数。
  • [0] * num : 初始化num个0的列表
  • list(range(1, 10 ** n))range()方法生成可迭代对象,在使用list()转为列表
  • 快速生成列表
    dp = [arr[i][1] for i in range(n)]
  • sorted():返回一个新的已排序的列表,参数 key = str.lower(每个元素中提取用于比较的键), reverse = True:反向进行排序
    也可以指定关键字排序:arr.sort(key=lambda x : (x[0], x[1]))
  • reduce(gcd, vals):将两个参数的 function 从左至右积累地应用到 iterable 的条目,以便将该可迭代对象缩减为单一的值。 例如,reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) 是计算 ((((1+2)+3)+4)+5) 的值。 左边的参数 x 是积累值而右边的参数 y 则是来自 iterable 的更新值。
  • gcd(x, y):求最大公约数
  • 两个列表相加相当于列表合并
  • itertools.product(*iterables, repeat=1):
可迭代对象输入的笛卡儿积。

大致相当于生成器表达式中的嵌套循环。例如, product(A, B)((x,y) for x in A for y in B) 返回结果一样。

嵌套循环像里程表那样循环变动,每次迭代时将最右侧的元素向后迭代。这种模式形成了一种字典序,因此如果输入的可迭代对象是已排序的,笛卡尔积元组依次序发出。

要计算可迭代对象自身的笛卡尔积,将可选参数 repeat 设定为要重复的次数。例如,product(A, repeat=4) 和 product(A, A, A, A) 是一样的。
list(product(range(3),repeat=1))
[(0,), (1,), (2,)]
  • itertools.combinations(iterable, r)
    返回由输入 iterable 中元素组成长度为 r 的子序列。
    组合元组会以字典顺序根据所输入 iterable 的顺序发出。 因此,如果所输入 iterable已排序的,组合元组也将按已排序的顺序生成。
    即使元素的值相同,不同位置的元素也被认为是不同的。如果元素各自不同,那么每个组合中没有重复元素

  • any(iterable)
    如果 iterable 的任一元素为真值则返回 True。 如果可迭代对象为空,返回 False。 等价于:

def any(iterable):
    for element in iterable:
        if element:
            return True
    return False
  • zip(*iterables)

创建一个聚合了来自每个可迭代对象中的元素的迭代器。

返回一个元组的迭代器,其中的第 i 个元组包含来自每个参数序列或可迭代对象的第 i 个元素。
当所输入可迭代对象中最短的一个被耗尽时,迭代器将停止迭代。 当只有一个可迭代对象参数时,它将返回一个单元组的迭代器。
不带参数时,它将返回一个空迭代器。 相当于:

zip():将列表对应位置的每个元素,压缩为一个元组。
zip(*):解压缩
zip() 与 * 运算符相结合可以用来拆解一个列表:

>>> x = [1, 2, 3]
>>> y = [4, 5, 6]
>>> zipped = zip(x, y)
>>> list(zipped)
[(1, 4), (2, 5), (3, 6)]
>>> x2, y2 = zip(*zip(x, y))
>>> x == list(x2) and y == list(y2)
True

例如

A = ["cba","daf","ghi"]
zip(*A):["cdg", "bah", "afg"]

ans = 0
for col in zip(*A):
     if any(col[i] > col[i+1] for i in range(len(col)-1)):
         ans += 1
 return ans
  • 计算椭圆相交面积

from shapely.geometry import Polygon
p1=Polygon([(0,0),(1,1),(1,0)])
p2=Polygon([(0,1),(1,0),(1,1)])
print p1.intersects(p2)
x = p1.intersection(p2)
x.area # 从相交处生成的多边形

典型例题

判断回文数

算法思想:
 一、set集合:回文串关于中心对称。其特点是除了长度为奇数的回文串中间的那个字符,其余字符若在前边出现一次,则必然在后边也出现一次。所以某个字符串若为回文串的一个排列,则其中至多有一个孤立的字符,其余字符应两两成对。
据此,可创建一个空集合,然后从头到尾扫描字符串。若扫描到的字符不在集合中,将其放入集合;若扫描到的字符在集合中,则可与其配成一对,为此从集合中移除该字符。
当扫描完字符串时,若集合中元素个数小于2(也就是集合为空或集合中只有一个元素),则该字符串中的所有字符可以构成一个回文串,也就是该字符串是回文串的一个排列。反之,则不是。

 def canPermutePalindrome(self, s: str) -> bool:
        myset = set()
        for ch in s:
            if ch in myset:
                myset.remove(ch)
            else:
                myset.add(ch)
        return len(myset) < 2

 二、位运算:出现重复的字符(转化为1作业多少位),异或之后为0,最后可能只剩一个字符,若该字符是移位后是2的n次方即是回文数

def canPermutePalindrome(self, s: str) -> bool:
     result = 0
     for c in s:
        result ^= 1<<ord(c)
     return (result & (result - 1)) == 0

x的平方

换底公式:x ^ 2 = e ^ 1/2 * lnx
二分查找:由于x的平方根整数部分ans满足k^2<=x的最大值k,因此对k进行二分查找

二进制求和

把 aa 和 bb 转换成整型数字 xx 和 yy,在接下来的过程中,xx 保存结果,yy 保存进位。
当进位不为 0:
计算当前 x 和 y 的无进位相加结果:answer = x ^ y
计算当前 x 和 y 的进位:carry = (x & y) << 1
完成本次循环,更新 x = answer,y = carry
返回 x 的二进制形式
为什么这个方法是可行的呢?
在第一轮计算中,answer 的最后一位是 x 和 y 相加之后的结果,carry 的倒数第二位是 x 和 y 最后一位相加的进位。
接着每一轮中,由于 carry 是由 x 和 y 按位与并且左移得到的,那么最后会补零,所以在下面计算的过程中后面的数位不受影响,
而每一轮都可以得到一个低 i 位的答案和它向低 i + 1 位的进位,也就模拟了加法的过程。
def addBinary(self, a, b) -> str:
        x, y = int(a, 2), int(b, 2)
        while y:
            answer = x ^ y
            carry = (x & y) << 1
            x, y = answer, carry
        return bin(x)[2:]

汉诺塔

1~N 从 A 移动到 B,C 作为辅助
等价于:
1,1~N-1 从 A 移动到 C,B 为辅助
2,把 N 从 A 移动到 B
3,1~N-1 从 C 移动到 B,A 为辅助

二分查找

全范围内二分查找
等价于三个问题
左边比(递归)
中间比
右边找(递归)
int mid = (low+high) >>> 1; // 防止溢出,移位也更高效

希尔排序

  • 思路:如序列 9 8 7 6 5 4 3 2 1
    确定一个增量序列,如 4(length / 2) 2 1,从大到小使用增量

  • 使用第一个增量将序列划分若干子序列,下标组合为 0-4-8, 1-5,2-6,3-7

  • 依次对子序列使用直接插入排序

  • 使用第二个增量,将序列划分为若干子序列(0-2-4-6-8),(1-3-5-7)

  • 依次使用直接插入排序

  • 使用第三个增量1,这时子序列就是原序列
    时间复杂度:O(nlogn)~O(n2)
    空间复杂度: O(1)
    原址排序
    稳定性: 由于相同元素可能划分至不同子序列单独排序,因此稳定性无法保证–不稳定

棋盘覆盖

问题: 在一个2 ^ k * 2 ^ k个方格组成的棋盘,恰有一个方格与其他方格不同,称该方格为特殊方格。显然该特殊方格在棋盘上出现共有4 ^ k种情形。在该特殊棋盘上需要用L型骨牌将除了特殊方格外的其他方格全部覆盖,且任何两个骨牌不能重叠,在一个期盼中,骨牌个数为 (4 ^ k-1) / 3。
分析: 当k>0时,主要是将一个2 ^ k * 2 ^ k的棋盘分为2 ^ k-1 * 2 ^ k -1的棋盘,将大问题递归为小问题。其中特殊方格必位于其中一个小方格,其余三个子棋盘无特殊方格,为了将这三个棋盘转化为子棋盘,可以用一个L型骨牌覆盖这个三个棋盘的汇合处。将棋盘化为2*2的时候便可以解决问题。

/**
     *
     * @param tr: 棋盘左上角方格行号
     * @param tc: 左上角列号
     * @param dr: 特殊方格所在行号
     * @param dc: 列号
     * @param size:棋盘规模size*size
     */
    public static void chess_board(int tr, int tc, int dr, int dc, int size) {
        if (size == 1)
            return;

        int t = tile++; // L型骨牌牌号
        int s = size / 2;  // 求解子问题

        // 覆盖左上角子棋盘
        if (dr < tr + s && dc < tc + s) {
            // 特殊方格 在此棋盘中, 继续缩小子问题
            chess_board(tr, tc, dr, dc, s);
        } else { // 此棋盘没有特殊方格
            // 用t号L型骨牌覆盖右下角
            board[tr + s - 1][tc + s - 1] = t;
            // 覆盖其余方格
            chess_board(tr, tc, tr + s - 1, tc + s - 1, s);
        }

        // 覆盖右上角子棋盘
        if (dr < tr + s && dc >= tc + s) {
            // 特殊方格 在此棋盘中, 继续缩小子问题
            chess_board(tr, tc + s, dr, dc, s);
        } else { // 此棋盘没有特殊方格
            // 用t号L型骨牌覆盖右下角
            board[tr + s - 1][tc + s] = t;
            // 覆盖其余方格
            chess_board(tr, tc + s, tr + s - 1, tc + s, s);
        }

        // 覆盖左下角子棋盘
        if (dr >= tr + s && dc < tc + s) {
            // 特殊方格 在此棋盘中, 继续缩小子问题
            chess_board(tr + s, tc, dr, dc, s);
        } else { // 此棋盘没有特殊方格
            // 用t号L型骨牌覆盖右上角
            board[tr + s][tc + s - 1] = t;
            // 覆盖其余方格
            chess_board(tr + s, tc, tr + s, tc + s - 1, s);
        }

        // 覆盖右下角子棋盘
        if (dr >= tr + s && dc >= tc + s) {
            // 特殊方格 在此棋盘中, 继续缩小子问题
            chess_board(tr + s, tc + s, dr, dc, s);
        } else { // 此棋盘没有特殊方格
            // 用t号L型骨牌覆盖左上角
            board[tr + s][tc + s] = t;
            // 覆盖其余方格
            chess_board(tr + s, tc + s, tr + s, tc + s, s);
        }
    }

完美素数

对于一个 正整数,如果它和除了它自身以外的所有 正因子 之和相等,我们称它为 「完美数」。
解法一:
枚举:从1到sqrt(num),枚举即可。这是因为如果num有一个大于sqrt(n)的因数,那么一定有一个小于sqrt(num)的因数n/x,特殊情况是x = n/x;
解法二
欧几里得-欧拉定理告诉我们,每个偶完全数都可以写成2^(p-1) * (2 ^ p - 1)的形式,其中 p为素数。例如前四个完全数可以写成如下形式:

6 = 2^1 * (2^2 - 1)
28 = 2^2 * (2^3 - 1)
496 = 2^3 * (2^4 - 1)
8128 = 2^4 * (2^5 - 1)

由于目前奇完全数还未被发现,因此所有的完全数都可以写成上述形式。当 n 不超过 10^8 时,p 也不会很大,因此我们只要带入最小的若干个素数 2, 3, 5, 7, 13, 17, 19, 31,将不超过 10 ^ 8 的所有完全数计算出来即可。
2 ^ n 相当于 1 << n 位

public boolean checkPerfectNumber(int num) {
     int[] primes=new int[]{2,3,5,7,13,17,19,31};
     for (int prime: primes) {
         if ((1 << (prime-1))* ((1 << prime) - 1) == num)
             return true;
     }
     return false;
 }

十进制整数的反码

  • 解法一:
    原理:原码+反码 = 2^n-1 n为二进制位数
    循环条件:num <= N,即可求出2^n
if(N==0)
  return 1;
int num = 1;
while(num <= N) {
	num <<= 1;
}
return num-1-N;
  • 解法二:
    2^n-1 = N
    n = log2^N+1;
    n为长度
    由于Math类中无log2,默认log以e为底,log10以10为底,因此使用换底公式log2^x=log e ^ x / log e ^ 2;
public int bitwiseComplement(int N) {
   if (N == 0)
        return 1;
    int length = (int)(Math.log(N) / Math.log(2)) + 1;
    return (int)Math.pow(2, length) - 1 - N;
}
  • 解法三
    根据反码的定义 等于原码取反
    原码与对应二进制位数的全与1异或就是反码
public int bitwiseComplement(int N) {
	int num=1;
	while(num<N) {
	   num = (num<<1)+1;
	}
	return num ^ N;
}

小白上楼梯

爬楼梯一次可以爬一步,两步,三步,问爬到n阶台阶的方法

static int f(int n) {
	if (n == 0) return 1;
	if (n == 1) return 1;
	if (n == 2) return 2;
	return f(n-1)+f(n-2)+f(n-3);
}

小白上楼梯2

爬楼梯每次至少爬d阶,爬到n阶台阶的方案数
动态规划,每次向前推d,第i-d的阶数方案会对下一次造成影响。

int sum = 1;
int mod = 1000000007;
System.out.println(mod);
for (int i = d; i <= n; i++) {
    sum += x[i-d];
    x[i] += sum;
    System.out.print(x[i]+" ");
    x[i] %= mod;
    sum %= mod;
}
System.out.println(x[n]);

最大字段和

给定序列,找出最大的子段和
累加每个元素,当sum<0,将其初始化为0

long sum = 0, max = 0;
for (int i = 0; i < n; i++) {
     sum += x[i];
     max = Math.max(max, sum);
     if (sum < 0)
         sum = 0;
 }
 System.out.println(max);

最长公共子串

在字符串中任意个连续的字符组成的子序列成为该串的子串,给定两个字符串,求出最长的公共子串的长度
思想:从头遍历一个字符串,当字符串不包含则将指针++

String s = sc.nextLine();
String x = sc.nextLine();
 int max = 0, i = 0;
 StringBuilder sb = new StringBuilder();
 for (char c : s.toCharArray()) {
     sb.append(c);
     String ss = sb.substring(i);
     if (x.contains(ss)) {
         max = Math.max(max, ss.length());
     } else {
         i++;
     }
 }
 System.out.println(max);

最长公共子序列

  • 最长公共子序列(longest common sequence)和最长公共子串(longest common substring)不是一回事儿。什么是子序列呢? 即一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。什么是子串呢 给定串中任意个连续的字符组成的子序列称为该串的子串。

  • 举个例子(S1={1,3,4,5,6,7,7,8}和S2={3,5,7,4,8,6,7,8,2}),

    • 假如S1的最后一个元素与S2的最后一个元素相等,那么S1和S2的LCS就等于 {S1减去最后一个元素} 与 {S2减去最后一个元素} 的 LCS 再加上 S1和S2相等的最后一个元素。
    • 假如S1的最后一个元素与S2的最后一个元素不等(本例子就是属于这种情况),那么S1和S2的LCS就等于: {S1减去最后一个元素} 与 S2 的LCS, {S2减去最后一个元素} 与 S1 的LCS 中的最大的那个序列。
int[] x = {0, 2, 5, 7, 3, 6, 8, 4};
int[] y = {0, 3, 4, 7, 3, 6, 4};
int[][] xx = new int[x.length+1][y.length+1];
for (int i = 1; i < x.length; i++) {
    for (int j = 1; j < y.length; j++) {
        if (x[i] == y[j])
            xx[i][j] = xx[i-1][j-1]+1;
        else
            xx[i][j] = Math.max(xx[i-1][j], xx[i][j-1]);
    }
}
System.out.println(xx[x.length-1][y.length-1]);
  • 输出最长公共子序列
    类似的倒推回去:当x[n] i= y[n]时,比较xx[i-1][j]与x[i][j-1]如果若大于就选i-1与j反之选择i与j-1,当等于时选择不同的方向有不同的结果。
int i = x.length-1, j = y.length-1;
StringBuilder sb = new StringBuilder();
while (xx[i][j] > 0) {
    if (x[i] == y[j]) {
        sb.append(x[i]);
        i--;
        j--;
    } else if (x[i] != y[j]) {
        if (xx[i - 1][j] > xx[i][j - 1]) {
            i--;
        } else {
            j--;
        }
    }
}
System.out.println(sb.reverse().toString());

最长递增子序列

  • DP-动态规划
    状态设计:用一个维护数组dp[i]表示以a[i]结尾的最长递增子序列的长度
    状态转移:之后向前找到一个小于a[i]的进行状态转移dp[i] = Math.max(dp[i], dp[j]+1);
    边界处理:dp[i]=1(0<=j<n)
    时间复杂度:O(n2)
int[] x = {3, 1, 2, 1, 8, 5};
// dp[i]表示以a[i]结尾的最长递增子序列的长度
dp = new int[x.length];
int ans = 0;
for (int i = 0; i < x.length; i++) {
    // 初始化每一个dp[i]=1
    dp[i] = 1;
    for (int j = 0; j < i; j++) {
        if (x[i] > x[j])
            dp[i] = Math.max(dp[i], dp[j]+1);  // 状态转移
    }
    ans = Math.max(ans, dp[i]);  // 比较每一个dp,取最大值
}
System.out.println(ans);
  • 贪心+二分查找
    利用贪心的思想,对于一个上升的子序列,显然当前最后一个元素越小,越有利于添加新的元素,这样LIS长度自然更长。
    因此,我们只需要维护dp数组,其表示就是长度为i+1的LIS结尾元素的最小值,保证每一位都是最小值
    dp数组单调递增,因此对于每一个a[i],先判断是否可以直接插入到dp数组尾部,即比较其与dp数组的最大值,即最后一位;如果不可以则找出dp数组中第一个大于等于a[i]的位置,用a[i]替换之。
int[] dp2 = new int[x.length];
int index = 0;
dp2[0] = x[0];
for (int i = 1; i < x.length; i++) {
    if (x[i] > dp2[index])
        dp2[++index] = x[i];
    else
        dp2[bin(index, x[i])] = x[i];
}
System.out.println(index+1);

// 二分查找
public static int bin(int e, int x) {
int s = 0;
while (s < e) {
    int mid = (s + e) >>> 1;
    if (dp[mid] < x)
        s = mid+1;
    else
        e = mid-1;
}
return s;

数组中的最小差值

给你一个整数数组,给每一个元素加上任意一个数组x (-K <= x <= K),从而得到新数组,返回数组中最大值与最小值的最小差值
解析
最小化max(B) - min(B)也就是分别最小化max(B)和最大化min(B)。max(B)最小可能为max(A)-Kmin(B)最大化min(A)+K所以最小就是max(A) - min(A) - 2*K

范围求和

给定一个初始元素全部为 0,大小为 m*n 的矩阵 M 以及在 M 上的一系列更新操作。
操作用二维数组表示,其中的每个操作用一个含有两个正整数 a 和 b 的数组表示,含义是将所有符合 0 <= i < a 以及 0 <= j < b 的元素 M[i][j] 的值都增加 1。
在执行给定的一系列操作后,你需要返回矩阵中含有最大整数的元素个数。
解法
我们可以观察到最大元素会是两个操作对应矩阵的交集区域。我们还可以发现要求这块区域,我们不需要将操作区域一个一个加一,我们只需要记录交集区域的右下角即可。这个角的计算方法为:表示所有操作的 op[i]op[i] 中的最小值。
这样,最大元素的数目就是 x×y

for item in ops:
   m = min(m, item[0])
   n = min(n, item[1])
return m * n

奇数在左,偶数在右

思路:类似快速排序,左指针搜索偶数,右指针搜索奇数,交换,结束条件是两指针交叉。

阶乘尾数

求出阶乘获得尾数0的个数
n! 的尾数0 就是 不断累加 n/5 直到 n=0为止

class Solution {
    public int trailingZeroes(int n) {
        int ans = 0;
        while(n >= 5) {
            ans += n / 5;
            n /= 5;
        }
        return ans;
    }
}

矩阵连乘

多个矩阵相乘,乘的顺序不一样,计算的次数也不同
例如:给定三个连乘矩阵{A1,A2,A3}的维数分别是10 * 100,100 * 5和5 * 50,采用(A1A2)A3,乘法次数为10*100 * 5+10 * 5 * 50=7500次,而采用A1(A2A3),乘法次数为100 * 5 * 50+10 * 100 * 50=75000次乘法,显然,最好的次序是(A1A2)A3,乘法次数为7500次。

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] p = new int[n+1];
        for (int i = 0 ; i <= n; i++) {
            p[i] = sc.nextInt();
        }
        long[][] m = new long[n+1][n+1];
        for(int i = 1;i <= n;i++)//初始化,矩阵长度为1时,从i到i的矩阵连乘子问题只有一个矩阵,操作次数是0
        {
            m[i][i] = 0;
        }
        //不同规模的子问题
        for (int r = 2; r<= n; r++) {
        	//每一个规模为r的矩阵连乘序列的首矩阵Ai
            for (int i = 1; i <= n-r+1; i++) {  //从第i个矩阵开始,长度为r,则矩阵为(Ai-A(i+r-1))
                int j = i + r - 1; //每一个规模为r的矩阵连乘序列的尾矩阵Aj
                m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j]; // 决策为k=i的乘法次数
                //对Ai...Aj的所有决策,求最优值,记录最优决策
                for (int k = i+1; k < j; k++) {
                    long cur = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
                    if (cur < m[i][j])
                        m[i][j] = cur;
                }
            }
        }
        System.out.println(m[1][n]);
    }
}

Palindrome

求最少增加几个字符使得给定的字符串形成回文串
分析:为该串与该串的倒序的最长公共子序列
由于最长公共子序列,每次只需要前面的值,所以数组长度为2,节省空间

import java.util.Scanner;
 
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        sc.nextLine();
        String s = sc.nextLine();
        StringBuilder sb = new StringBuilder(s);
        char[] x = sb.toString().toCharArray();
        char[] y = sb.reverse().toString().toCharArray();
         
        int[][] dp = new int[2][x.length+1];
        for(int i = 0; i < x.length; i++) {
            for (int j = 0; j < y.length; j++) {
                if (x[i] == y[j])
                    dp[(i+1)%2][j+1] = dp[i%2][j]+1;
                else
                    dp[(i+1)%2][j+1] = Math.max(dp[i%2][j+1], dp[(i+1)%2][j]);
            }
        }
        System.out.println(s.length() - dp[x.length%2][x.length]);
    }
}

完全背包

有一个背包容量有限,不同的物品有不同的价值需要不同的容量,求使背包的容量的最大价值。动态规划:当前物品,可以选择加入或不加入两种选择,不加入则价值等于dp[i-1][j],加入的话则是:dp[i-1][j-w[i]]+v[i]与dp[i-1][j]的最大值

import java.util.Scanner;
 
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int W = sc.nextInt();
        int[] w = new int[n];
        int[] v = new int[n];
        for (int i = 0; i < n; i++) {
            w[i] = sc.nextInt();
            v[i] = sc.nextInt();
        }
        int[][] dp = new int[n][W+1];
        for (int i = 0; i <= W; i++) {
            dp[0][i] = i/w[0]*v[0];
        }
        int max = 0;
        for (int i = 1; i < n; i++) {
            for (int j = 1; j <= W; j++) {
                for (int k = 0; k*w[i] <= j; k++) {
                    int t = k*v[i]+dp[i-1][j-k*w[i]];
                    if (t > max)
                        max = t;
                }
                dp[i][j] = max;
                max = 0;
            }
        }
        System.out.println(dp[n-1][W]);
    }
}

图像压缩

  • 问题描述

  • 图像压缩的问题我们是这样理解的:大家都知道计算机的图像是用灰度值序列来表示的{P1,P2…Pn},其中Pi表示像素点 i的灰度值。而通常灰度值的范围是0~255,因此需要8位二进制数来表示一个像素。这个时候大家应该有了一些小的疑问:我能不能用更少的位数来表示灰度值?(因为有的灰度值并没有达到255这么大)所以我们引入了图像压缩算法来解决这个问题。

  • 算法设计的知识
    我们要将灰度值序列分组,而每一组中所有的数就有可能是 <255 的,所以我们就不需要用8位数字去表示像素大小了,但是分组会带来一个新的问题:我如何表示当前组中像素的个数和像素的位数呢(因为不是八位,所以要有一个数据来记录真正的位数)?这里我们引入两个固定位数的值来表示,①我们用3位数字来表示当前组的每一位像素的的位数②我们引入8来表示当前组中像素点的个数。

  • 因为我们在这里规定了一组中最多存储–>0~255个数字,而一个灰度值最多有8位(2^3),所以我们可以用即3位数字来表示当前组的像素位数(注意这里都是二进制)

例如:
{6, 5, 7,5, 245, 180, 28,28,19, 22, 25,20}这是一组灰度值序列。我们按照默认的解体方法来看----一共12个数字,所以12*8=96位来表示。

而下面我们将其进行分组
这里我们将他们分为三组:

第一组4个数,最大是7所以用3位表示;

第二组2个数,最大是245所以用8位表示;

第三组6个数,最大是28所以用5位表示;

这个时候,我们最后得到了最后的位数结果为:43+28+65+113=91。是不是优化了??

  • 那我们算法应该怎么做来找最优的值呢??

既然是DP问题,所以我们肯定需要数组来记录每一步的最优值。这里我们用S[n]来记录第i个数字的最优处理方式得到的最优解。l[n] 中来记录第当前第i个数所在组中有多少个数。而b[n] 中存的数为当前组的像素位数

在这里插入图片描述
在求解过程中,我们知道在我们求s[3]的时候,我们是分三种情况
①前三个数为一组,这个时候我需要的存储位数是3(位数)*3(每一组中数的个数)+11(每分一组所必须的固定位数)

②s[1]为单独一组,剩下的两个数字为一组,此时我所需要的空间为s[1]+2*3+11

③前两个数字为一组,最后一个数为一组。此时我们要用s[2](前面已经计算出的最优值)+3*1+11

然后比较三个数的大小,取最小的那一种分组情况,然后记下l[3]=3(当前最优分组中是三个数在同一组中),b[3]=3(每一个像素所用的存储位数)。

代码

import java.util.Scanner;

public class oj1246 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] x = new int[n+1];
        long[] dp = new long[n+1];  // 动态规划数组,记录第i个数字的最优处理方式
        int[] b = new int[n+1];  // 代表第i个数的二进制位数

        for (int i = 1; i <= n; i++) {
            x[i] = sc.nextInt();
            // 获取当前数的二进制位数
            b[i] = (int)Math.ceil(Math.log(x[i]+1) / Math.log(2));
        }

        // 从第一个数开始
        for (int i = 1; i <= n ; i++) {
            int bmax = b[i];  // 当前二进制位数
            // 初始化前i-1个数一组
            dp[i] = dp[i-1] + bmax;

            // 前i-j个数为一组,需要求出之间最大值的位数
            // 从后往前推,首先 前i-1个数位一组,最后一个单独一组,之后前i-2个数一组,倒数后两个一组,j代表最后一组的个数
            for (int j = 2; j <= i && j < 256; j++) {
                if (bmax < b[i-j+1])
                    bmax = b[i-j+1];
                if (dp[i] > dp[i-j] + j*bmax) {
                    dp[i] = dp[i-j] + j*bmax;
                }
            }
            dp[i] += 11;  // 求完后加11
        }
        System.out.println(dp[n]);
    }
}

位运算

  • << >> 将二进制位进行左移右移
  • >>>’ 右移不带符号用0填充高位,>> 运算符用符号位填充高位
  • 对于int型,1<<351<<3是相同的,int 32 位,左边数是long型需要对右边数模64
  • :都为1结果为1,:有一个为1结果为1,异或:二者不同时结果1
  • 判断奇偶x&1 == 1 **?**奇数 **:**偶数
  • 1<<n = 2^n,n << 1 = 2n
  • 算术右移:数字向右移动,高位以符号位填充,低位越界后舍弃; == x/2向下取整
  • 逻辑右移:高位以0填充
  • 异或可以理解为: 1+0 = 1, 1+1=0, 0+0=0,相异为1
    性质交换律,可任意交换运算因子的位置,结果不变、结合律、对于任何x 都有 x^x = 0, x ^ 0 = x自反性:A ^ B ^ B = A ^ 0 = A
  • (x-1) & x:消掉最低位的1,也可判断是否是2的n次方
  • even = x&0xaaaaaaaa; // 和1010 1010 1010…做与运算求出偶数位,二进制的4位相当于16进制的1位
    odd = x&0x55555555; // 取出奇数位

最接近点对

一维问题: 在一组点中,找出最近点对,该对点距离最小。
分析:使用分治法“平衡子问题”,选择适当的分割点,分为两部分,分别求出每部分的最近点距离,另外在,分割边界出可能也存在两点距离最近。

static int[] x;
static int min = 0x7fffffff;
public static void main(String[] args) {
    x = new int[]{1, 3, 5, 6, 7, 8, 10, 11};
    minDis(0, x.length-1);
    System.out.println(min);
}

public static void minDis(int l, int r) {
    if (l+1 == r) {
        min = Math.min(min, x[r]-x[l]);
        return;
    }

    int mid = (l + r) >>> 1;
    min = Math.min(min, x[mid+1]-x[mid]);
    minDis(l, mid);
    minDis(mid+1, r);
}

二维问题

class Node implements Comparable<Node> {
    int x;
    int y;

    public Node(int xx, int yy) {
        x = xx;
        y = yy;
    }

    @Override
    public int compareTo(Node o) {
        if (this.x < o.x)
            return -1;
        else if (this.x > o.x)
            return 1;
        else{
            if (this.y < o.y)
                return -1;
            else if (this.y > o.y)
                return 1;
            else
                return 0;
        }
    }
}
    static double dmin = 0x7fffffff;
    static Node[] xx;
    
    // 二维,递归函数
    public static void minDisTwoDimension(int l, int r) {
        // 递归结束条件
        if (l <= r)
            return;
        // 子问题最小规模,找出两点距离,更新最小值
        if (l + 1 == r)
            dmin = Math.min(dmin, getDis(l, r));

        int mid = (l + r) >>> 1;

        // 递归求解子问题,缩小范围
        minDisTwoDimension(l, mid);
        minDisTwoDimension(mid+1, r);

        // 另外在mid左右两边分别可能存在一个点,该两点之间距离小于当前dmin
        // 因此因为mid两边之间距离小于dmin,因此 mid-dmin  ~ mid + dmin
        // 所以要找到该两端点的下标
        int left = binary1(l, r, (long) (xx[mid].x - dmin)), right = binary2(l, r, (long) (xx[mid].x - dmin));
        // 从边界出找最近点距离
		allpd(left, right);
    }
	// 遍历中间值两侧的所有点
    static void allpd(int l, int r) {
        for (int i = l<=0?1:l; i <= r; i++)
            for (int j = i + 1; j <= r; j++) {
                dmin = Math.min(dmin, getDis(i, j));
            }
    }

    // 找到左边距离中间值的下标
    public static int binary1(int l, int r, long d) {
        while (l < r) {
            int mid = (l + r) >>> 1;
            if (xx[mid].x >= d)
                r = mid - 1;
            else
                l = mid + 1;
        }
        return l;
    }

    // 找到右边距离中间值的下标
    public static int binary2(int l, int r, long d) {
        while (l < r) {
            int mid = (l + r) >>> 1;
            if (xx[mid].x <= d)
                l = mid + 1;
            else
                l = mid - 1;
        }
        return l;
    }
   // 两点间距离
    public static double getDis(int l, int r) {
        return Math.sqrt((xx[l].x - xx[r].x) * (xx[l].x - xx[r].x) + (xx[l].y - xx[r].y) * (xx[l].y - xx[r].y));
    }

递归

  • 找重复
    1.找到一种划分方法
    2.找到递归公式或者等价转化
    都是父问题化为求解子问题

  • 找变化的量
    变化的量要作为参数

  • 找出口
    根据参数变化的趋势,对边界进行控制

分治法

分治法(divide and conquer),将原问题划分成若干规模较小而结构与原问题一致的子问题,递归的解决这些子问题,然后再合并其结果就得到原问题的解。
确定运行时间是分治法的优点之一
分治法在每一层递归的步骤:

  • 分解(divide):将原问题分解成一系列子问题
  • 解决(conquer):递归的解决各问题。若子问题足够小,则直接有解
  • 合并(Combine):将子问题的结果合并成原问题的解
    主定理:(master theorem)提供了用渐近符号表示许多由分治法得到的递推关系式的方法。
    假设有递推关系式 ,其中n为问题规模,a为递推的子问题数量, n/b为每个子问题的规模(假设每个子问题的规模基本一样),f(n) 为递推以外进行的计算工作。
    (1):f(n):O(n ^ log b ^ a - e),那么T(n)= O(n^log b ^ a)
    (2):T(n) = O(n log b ^ a)

动态规划

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。

概念

动态规划(英语: Dynamic programming,简称 DP) 是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划算法的核心就是记住已经解决过的子问题的解;而记住求解的方式有两种:

自顶向下的备忘录法

比如:斐波拉契数列 Fibonacci。

public static int fibonacci(int n) {
	if (n <= 1)
		return 1;
	if (n == 2)
		return 2;
	return fibonacci(n-1) + fibonacci(n-2);
}

我们分析以前写过的递归就会发现有很多节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。

public class Fibonacci {
	public static void main(String[] args) {
		//创建备忘录
		int[] memo = new int[n+1];
		System.out.println(fibonacci(7));
	}
	/**
	 * 自顶向下备忘录法
	 * @param n
	 * @param memo	备忘录
	 * @return
	 */
	public static int fibonacci(int n, int[] memo) {
		// 如果已经求出了fibonacci(n)的值直接返回
		if(memo[n] != 0) return memo[n];
		// 否则将求出的值保存在 memo 备忘录中。
		if(n<=2)
			memo[n]=1;
		else {
			memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo);
		}
		return memo[n];
	}
}

这个方法是由上至下,比如求f(5),我们要求f(4)和f(3),求出来后放入备忘录,当求f(4)时需要f(3)和f(2),我们可以直接从备忘录取f(3)而不是再去求一遍。

自底向上的动态规划

备忘录法是利用了递归,上面算法不管怎样,计算 fib(6)的时候最后还是要计算出 fib(1), fib(2), fib(3) ……,那么何不先计算出 fib(1), fib(2), fib(3) ……,呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。

public class FibonacciPlus {
	/**
	 * 自底向上的动态规划
	 * @param n
	 * @return
	 */
	public static int fib(int n) {
		if(n<=0)return -1;
		//创建备忘录
		int[] memo = new int[n+1];
		memo[0]=0;
		memo[1]=1;
		for(int i=2;i<=n;i++) {
			memo[i]=memo[i-1]+memo[i-2];
		}
		return memo[n];
	}
	/**
	 * 参与循环的只有 i, i-1 , i-2 三项,可以优化空间
	 * @param n
	 * @return
	 */
	public static int fibPlus(int n) {
		if(n<=0)return -1;
		int memo_i_2=0;
		int memo_i_1=1;
		int memo_i=1;
		for(int i=2;i<=n;i++) {
			memo_i = memo_i_1+memo_i_2;
			memo_i_2 = memo_i_1;
			memo_i_1 = memo_i;
		}
		return memo_i;
	}
}

例题

区域和检索-数组不可变

问题描述
给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。

解法:
- 暴力循环从下标i到j求和,如果检索次数较多,则会超出时间限制。
- 降低时间复杂度,最理想情况O(1),求前缀和,sumRange(i, j) = (0-j+1)-(0-i-1的和)

代码
Java版

class NumArray {
    int[] sums;

    public NumArray(int[] nums) {
        int n = nums.length;
        sums = new int[n + 1];
        for (int i = 0; i < n; i++) {
            sums[i + 1] = sums[i] + nums[i];
        }
    }
    
    public int sumRange(int i, int j) {
        return sums[j + 1] - sums[i];
    }
}

Python版

class NumArray:

    def __init__(self, nums: List[int]):
        self.sums = [0]
        _sums = self.sums

        for num in nums:
            _sums.append(_sums[-1] + num)

    def sumRange(self, i: int, j: int) -> int:
        _sums = self.sums
        return _sums[j+1] - _sums[i]

复杂度分析

时间复杂度
初始化需要O(n),每次检索、O(1),其中n是nums的长度。
初始化需要检索遍历数组nums的前缀和,时间复杂度O(n)。

空间复杂度:
空间复杂度:O(n)O(n),其中 nn 是数组 \textit{nums}nums 的长度。需要创建一个长度为 n+1n+1 的前缀和数组

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Python算法竞赛模板是指在Python编程语言中,为了提高代码效率和简洁性,在算法竞赛中常用的一种代码结构和技巧的总结和应用。 一般来说,Python算法竞赛模板包含以下内容: 1. 导入必要的模块和库:在算法竞赛中,常常需要使用到一些常用的模块和库,比如math、collections等。在模板中,首先需要导入这些模块和库,以便后续的代码编写和使用。 2. 读入输入和处理:算法竞赛通常会给出一些输入数据,比如数组、矩阵、图等。在模板中,需要根据题目要求进行输入读取和处理。这部分代码主要负责将输入数据保存在变量中,并进行预处理,以便后续的算法和逻辑处理。 3. 算法和逻辑处理:这部分是整个模板的核心部分,主要是根据题目要求设计算法和逻辑处理的代码。在算法竞赛中,常用的算法包括贪心、动态规划、深度优先搜索、广度优先搜索等。根据具体的题目要求,选择合适的算法进行实现和应用。 4. 输出结果:在算法竞赛中,常常需要输出计算结果。在模板中,需要编写输出代码,将计算得到的结果输出到标准输出或文件中。 5. 主函数和调用:为了能够方便地测试和调用代码,在模板中通常要定义一个主函数,并在主函数中调用前面编写的函数和代码。主函数通常用来读取输入、调用处理和计算的函数,并输出结果。 Python算法竞赛模板的好处在于能够提高代码的复用性和可维护性。通过事先总结和编写模板,可以减少在比赛过程中的代码重复和错误,提高编写效率和代码质量。同时,模板可以帮助选手更好地理解和应用常用的算法和数据结构,提升解题能力。 当然,Python算法竞赛模板只是一种常见的代码结构和技巧总结,具体的应用还需根据不同的比赛和题目要求进行调整和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chaser&upper

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值